Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use_frameworks!
platform :ios, '9.0'

target 'Structure_IOS' do
pod 'Alamofire', '~> 4.5'
pod 'AlamofireObjectMapper', '~> 4.1.0'
pod 'Alamofire', '~> 4.7.1'
pod 'AlamofireObjectMapper', '~> 5.0.0'
pod 'SwiftLint'
pod 'RxSwift', '~> 4.1.2'
pod 'RxCocoa', '~> 4.1.2'
pod 'SVProgressHUD', '~> 2.2.5'
end
83 changes: 74 additions & 9 deletions src/Structure_IOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/Structure_IOS/Common/ViewModelType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// ViewModelType.swift
// Structure_IOS
//
// Created by DaoLQ on 4/10/18.
// Copyright © 2018 DaoLQ. All rights reserved.
//

import Foundation

protocol ViewModelType {
associatedtype Input
associatedtype Output

func transform(input: Input) -> Output
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation

enum BaseError: Error {

case networkError
case httpError(httpCode: Int)
case unexpectedError
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
import Foundation
import Alamofire

class CustomRequestAdapter: RequestAdapter {
final class CustomRequestAdapter: RequestAdapter {
private let userDefault = UserDefaults()

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest

// urlRequest.setValue(MY_API_KEY, forHTTPHeaderField: "X-AccessToken")
if let accessToken = userDefault.string(forKey: Constants.keyAccessToken) {
urlRequest.setValue(accessToken, forHTTPHeaderField: "X-AccessToken")
}
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
return urlRequest
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// BaseUploadRequest.swift
// Structure_IOS
//
// Created by DaoLQ on 4/13/18.
// Copyright © 2018 DaoLQ. All rights reserved.
//

import Foundation
import ObjectMapper

class BaseUploadRequest: NSObject {

var url = ""
var parameters: [String: Any]?
var files: [File]?

init(url: String) {
self.url = url
}

init(url: String, files: [File]) {
self.url = url
self.files = files
}
}

struct File {
var key: String
var path: String
}
95 changes: 71 additions & 24 deletions src/Structure_IOS/Data/Source/Remote/Service/APIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import UIKit
import Alamofire
import ObjectMapper
import RxSwift

struct APIService {

Expand All @@ -24,39 +25,85 @@ struct APIService {
alamofireManager.adapter = CustomRequestAdapter()
}

func request<T: Mappable>(input: BaseRequest, completion: @escaping (_ value: T?,_ error: BaseError?) -> Void) {
func request<T: Mappable>(input: BaseRequest) -> Observable<T> {

print("\n------------REQUEST INPUT")
print("link: %@", input.url)
print("body: %@", input.body ?? "No Body")
print("------------ END REQUEST INPUT\n")
alamofireManager.request(input.url, method: input.requestType, parameters: input.body, encoding: input.encoding)
.validate(statusCode: 200..<500)
.responseJSON { response in
print(response.request?.url ?? "Error")
print(response)
switch response.result {
case .success(let value):
if let statusCode = response.response?.statusCode {
if statusCode == 200 {
let object = Mapper<T>().map(JSONObject: value)
completion(object, nil)
} else {
if let error = Mapper<ErrorResponse>().map(JSONObject: value) {
completion(nil, BaseError.apiFailure(error: error))

return Observable.create { observer in
self.alamofireManager.request(input.url, method: input.requestType,
parameters: input.body, encoding: input.encoding)
.validate(statusCode: 200..<500)
.responseJSON { response in
print(response.request?.url ?? "Error")
print(response)
switch response.result {
case .success(let value):
if let statusCode = response.response?.statusCode {
if statusCode == 200 {
if let object = Mapper<T>().map(JSONObject: value) {
observer.onNext(object)
}
} else {
completion(nil, BaseError.httpError(httpCode: statusCode))
if let object = Mapper<ErrorResponse>().map(JSONObject: value) {
observer.onError(BaseError.apiFailure(error: object))
} else {
observer.onError(BaseError.httpError(httpCode: statusCode))
}
}
} else {
observer.on(.error(BaseError.unexpectedError))
}
observer.onCompleted()
case .failure:
observer.onError(BaseError.networkError)
observer.onCompleted()
}
}
return Disposables.create()
}
}

func upload(input: BaseUploadRequest) -> Observable<Void> {

print("\n------------ UPLOAD INPUT")
print("link: %@", input.url)
print("body: %@", input.parameters ?? "No Body")
print("------------ END UPLOAD INPUT\n")

return Observable.create({ observer in
self.alamofireManager.upload(multipartFormData: { multipartFormData in
if let parameters = input.parameters {
for (key, value) in parameters {
multipartFormData.append("\(value)".data(using: String.Encoding.utf8)!, withName: key)
}
}
if let files = input.files {
files.forEach { file in
if let url = URL(string: file.path) {
multipartFormData.append(url, withName: file.key)
}
} else {
completion(nil, BaseError.unexpectedError)
}
break
}
}, usingThreshold: UInt64.init(), to: input.url) { result in
switch result {
case .success(let upload, _, _):
upload.responseJSON { response in
if let error = response.error {
observer.onError(error)
return
}
observer.onNext()
observer.onCompleted()
}
case .failure(let error):
completion(nil, error as? BaseError)
break
observer.onError(error)
observer.onCompleted()
}
}
}
return Disposables.create()
})
}
}
65 changes: 65 additions & 0 deletions src/Structure_IOS/Extensions/ObservableExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// ObservableExtension.swift
// Structure_IOS
//
// Created by DaoLQ on 4/11/18.
// Copyright © 2018 DaoLQ. All rights reserved.
//

import Foundation
import RxSwift
import RxCocoa
import SVProgressHUD

extension ObservableType where E == Bool {
/// Boolean not operator
public func not() -> Observable<Bool> {
return self.map(!)
}

}

extension SharedSequenceConvertibleType {
func mapToVoid() -> SharedSequence<SharingStrategy, Void> {
return map { _ in }
}
}

extension ObservableType {

func catchErrorJustComplete() -> Observable<E> {
return catchError { _ in
return Observable.empty()
}
}

func asDriverOnErrorJustComplete() -> Driver<E> {
return asDriver { _ in
return Driver.empty()
}
}

func mapToVoid() -> Observable<Void> {
return map { _ in }
}
}

extension ObserverType where E == Void {
public func onNext() {
onNext(())
}
}

extension Reactive where Base: SVProgressHUD {

/// Bindable sink for `show()`, `hide()` methods.
public static var isAnimating: Binder<Bool> {
return Binder(UIApplication.shared) { _, isVisible in
if isVisible {
SVProgressHUD.show()
} else {
SVProgressHUD.dismiss()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension UIViewController {
view.addGestureRecognizer(tap)
}

func dismissKeyboard() {
@objc func dismissKeyboard() {
view.endEditing(true)
}
}
24 changes: 9 additions & 15 deletions src/Structure_IOS/Repositories/UserRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,24 @@

import Foundation
import ObjectMapper
import RxSwift

protocol UserRepository {
func searchUsers(keyword: String, limit: Int, completion: @escaping (BaseResult<SearchResponse>) -> Void)
func searchUsers(input: SearchRequest) -> Observable<[User]>
}

class UserRepositoryImpl: UserRepository {

private var api: APIService?
private var api: APIService!

required init(api: APIService) {
self.api = api
}

func searchUsers(keyword: String, limit: Int, completion: @escaping (BaseResult<SearchResponse>) -> Void) {
let input = SearchRequest(keyword: keyword, limit: limit)

api?.request(input: input) { (object: SearchResponse?, error) in
if let object = object {
completion(.success(object))
} else if let error = error {
completion(.failure(error: error))
} else {
completion(.failure(error: nil))
}
}

func searchUsers(input: SearchRequest) -> Observable<[User]> {
return api.request(input: input)
.map({ (response: SearchResponse) -> [User] in
return response.users
})
}
}
18 changes: 14 additions & 4 deletions src/Structure_IOS/Scenes/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,43 @@
<state key="normal" title="Search">
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</state>
<connections>
<action selector="searchButtonClicked:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Y4L-8W-lw0"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LrL-Hi-txo">
<rect key="frame" x="31" y="455" width="313" height="25"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="25" id="iu5-kO-Aen"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Jyb-3E-qfH" firstAttribute="top" secondItem="FNf-c4-Gfi" secondAttribute="bottom" constant="11" id="4uy-Jo-RmV"/>
<constraint firstItem="Jyb-3E-qfH" firstAttribute="height" secondItem="FNf-c4-Gfi" secondAttribute="height" id="Ee6-wV-Zv3"/>
<constraint firstItem="XHg-5V-z5Q" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="11" id="GRz-gT-ip2"/>
<constraint firstItem="Jyb-3E-qfH" firstAttribute="trailing" secondItem="FNf-c4-Gfi" secondAttribute="trailing" id="JFS-qP-x9W"/>
<constraint firstItem="LrL-Hi-txo" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leadingMargin" constant="15" id="LbM-ye-j9Y"/>
<constraint firstItem="Jyb-3E-qfH" firstAttribute="leading" secondItem="FNf-c4-Gfi" secondAttribute="leading" id="PGc-zt-YmU"/>
<constraint firstItem="FNf-c4-Gfi" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" constant="2" id="Xyy-P2-zck"/>
<constraint firstAttribute="trailingMargin" secondItem="XHg-5V-z5Q" secondAttribute="trailing" constant="11" id="bG3-Pc-Npb"/>
<constraint firstItem="FNf-c4-Gfi" firstAttribute="top" secondItem="XHg-5V-z5Q" secondAttribute="bottom" constant="38" id="fDG-4t-zyt"/>
<constraint firstItem="XHg-5V-z5Q" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" constant="100" id="frd-tB-nCM"/>
<constraint firstItem="eNK-jz-vSZ" firstAttribute="centerX" secondItem="Jyb-3E-qfH" secondAttribute="centerX" id="hNK-5T-byD"/>
<constraint firstItem="eNK-jz-vSZ" firstAttribute="top" secondItem="Jyb-3E-qfH" secondAttribute="bottom" constant="45" id="jm4-U3-ROW"/>
<constraint firstAttribute="trailingMargin" secondItem="LrL-Hi-txo" secondAttribute="trailing" constant="15" id="kRi-fZ-9U0"/>
<constraint firstItem="LrL-Hi-txo" firstAttribute="top" secondItem="eNK-jz-vSZ" secondAttribute="bottom" constant="50" id="qba-c3-AMP"/>
<constraint firstAttribute="trailing" secondItem="FNf-c4-Gfi" secondAttribute="trailing" constant="2" id="uKg-wQ-ded"/>
</constraints>
</view>
<navigationItem key="navigationItem" title="Search" id="Tz2-Jl-gbE">
<barButtonItem key="backBarButtonItem" title="Back" id="2j0-4u-Pii"/>
</navigationItem>
<connections>
<outlet property="errorLabel" destination="LrL-Hi-txo" id="eei-Gc-CCw"/>
<outlet property="limitNumberTextField" destination="Jyb-3E-qfH" id="ZlW-Qv-42r"/>
<outlet property="searchButton" destination="eNK-jz-vSZ" id="1mW-lH-QiV"/>
<outlet property="searchButton" destination="eNK-jz-vSZ" id="Wbd-HZ-YVY"/>
<outlet property="searchTextField" destination="FNf-c4-Gfi" id="9XA-2z-Wo6"/>
</connections>
</viewController>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
import UIKit

class ListUsersTableViewCell: UITableViewCell {
static let reuseID = "ListUsersTableViewCell"

@IBOutlet weak var userNameLabel: UILabel!

func bind(viewModel: UserItemViewModel) {
self.userNameLabel.text = viewModel.login
}

func updateCell(user: User?) {
userNameLabel.text = user?.login
}
Expand Down
Loading