Swift 프로젝트 하면서 외국 블러그를 참조해서 만든 MVVM 클래스 모듈 입니다.

 

1. Model Protocol

import Foundation

// 프로토콜에 맞게 헤더 역활을 하는 데이터 값을 입력하면됨. 없어도 됨.
protocol APIModel {
    var rsltCD: Int? { get set }
    var rsltMsg: String? { get set }
}

typealias APIModelCodable = APIModel & Codable

extension APIModel where Self: Codable {
    
    static func from(json: String, using encoding: String.Encoding = .utf8) -> Self? {
        guard let data = json.data(using: encoding) else { return nil }
        return from(data: data)
    }
    
    static func from(data: Data) -> Self? {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        
        do {
            let decodeData = try decoder.decode(Self.self, from: data)
            return decodeData
            
        } catch {
            log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
        }
        
        return nil
    }
    
    var jsonData: Data? {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return try? encoder.encode(self)
    }
    
    var jsonString: String? {
        guard let data = self.jsonData else { return nil }
        return String(data: data, encoding: .utf8)
    }
}

2. View Model Protocol

import Foundation
import Alamofire

/*
Alamofire 5.6 버전을 사용했습니다.
*/

enum APIError: Error {
    case URL_ERROR
}

let TIMEOUT: TimeInterval = 10
let apiSerialQueue = DispatchQueue(label: "kr.kfcc.insurance")

/*
 개발환경이 폐쇄적인 경우 https 인증서 문제 발생시 사용(사설인증서)
 */
let session = {
    let manager = ServerTrustManager(allHostsMustBeEvaluated: false, evaluators: ["api.xxxxxxx.net": DisabledTrustEvaluator()])
    let configuration = URLSessionConfiguration.af.default
    return Session(configuration: configuration, serverTrustManager: manager)
}()

protocol APIRequest {
    associatedtype APIModelValue: APIModelCodable
    static var method: Alamofire.HTTPMethod { get set }
    static var suburl: API.WEB_SUBURL { get set }
    static func url() -> String
    static func generateStringData(_ params: Dictionary<String, Any>? ) -> Data?
    static func request(parameters: Dictionary<String, Any>?, completion: @escaping (_ object: APIModelValue?, _ error: Error?) -> Void)
    static func multipart_request(image: UIImage, withName: String, parameters: Dictionary<String, Any>?, completion: @escaping (_ object: APIModelValue?, _ error: Error?) -> Void)
}

extension APIRequest {
    
    static func url() -> String {
        return API.requestURL(subURL: Self.suburl)
    }
    
    static func generateStringData(_ params: Dictionary<String, Any>? ) -> Data? {
        // paramString 은 데이터 형식에 맞게 변경해서 사용하세요.
        var paramString: String = "test_mode=\(API_DEBUG_MODE)&user_key=\(0)&version=\("1.0.0")&platform=I&store_type=\("APPSTORE")&lang_cd=\(UIDevice.deviceLanguageCode!)&country_cd=\(UIDevice.deviceRegionCode!)&timestamp=\(Int(0))"
        
        if let p = params {
            for (key,value) in p {
                paramString = paramString + "&\(key)=\(value)"
            }
        }
        
        log(direction: .SEND, ofType: self, datas: paramString)
        return paramString.data(using: .utf8)
    }
    
    /// Alamofire Rest API Request
    static func request(parameters: Dictionary<String, Any>?, completion: @escaping (_ object: APIModelValue?, _ error: Error?) -> Void) {
        
        guard let url = URL.init(string: url()) else {
            completion(nil, APIError.URL_ERROR)
            return
        }
        
        let start  = CFAbsoluteTimeGetCurrent()
        
        var request: URLRequest = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.timeoutInterval = TIMEOUT
        request.setValue("1.0.0", forHTTPHeaderField: "X-API-Version")
        
        request.setValue("KR", forHTTPHeaderField: "X-Country")
        request.setValue("ko", forHTTPHeaderField: "X-API-Language")
        
        if let parameter = generateStringData(parameters) {
            request.httpBody = parameter
        }
        
//      session.request(request).validate().responseDecodable(of: APIModelValue.self) { response in
        AF.request(request).validate().responseDecodable(of: APIModelValue.self) { response in //responseJSON { (response) in
            
            let end = CFAbsoluteTimeGetCurrent()
            
            switch response.result {
            case .success(let obj):
                
                log(direction: .RECEIVE, ofType: self, datas: "API Request Data (\((end-start)))","\(url.absoluteString)","\(obj)")
                // obj 값은 자동으로 타입매칭 되서 넘어감.( APIModelValue.self)
                completion(obj, nil)
               
            case .failure(let error):
                completion(nil, error)
            }
        }
    }
    
    /// Alamofire Rest Multipart API Request
    /// 일반적인 JSON 데이터로 처리하는 방식 함수
    /// validate().responseJSON{(response)
    static func multipart_request(image: UIImage, withName: String, parameters: Dictionary<String, Any>?, completion: @escaping (_ object: APIModelValue?, _ error: Error?) -> Void) {
        
        let headers: HTTPHeaders = [
            "Content-type": "multipart/form-data"
        ]
        
        AF.upload(multipartFormData: { multipartFormData in
            if let params = parameters {
                for (key, val) in params {
                    
                    if let temp = val as? String {
                        multipartFormData.append(temp.data(using: .utf8)!, withName: key)
                    }
                    
                    if let temp = val as? Int {
                        multipartFormData.append("\(temp)".data(using: .utf8)!, withName: key)
                    }
                }
            }

            if let imageData = image.pngData() {
                multipartFormData.append(imageData, withName: withName, mimeType: "image/png")
            }
            log(direction: .ETC, ofType: self, datas: "Param : \(String(describing: parameters))", "withName : \(withName)")
            
        }, to: url(), method: .post, headers: headers
        ).uploadProgress(queue: .main, closure: { progress in
            print("Upload Progress : \(progress.fractionCompleted)")
        }).validate().responseJSON{(response) in
            switch response.result {
            case .success(let data):
                do {
                    if !JSONSerialization.isValidJSONObject(data) {
                        completion(nil, nil)
                        return
                    }
                    
                    let jsonData = try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
                    
                    log(direction: .RECEIVE, ofType: self, datas: "API Request Data","\(String(describing: parameters))","\(data)")
                    
                    completion(APIModelValue.from(data: jsonData), nil)
                    
                } catch(let error) {
                    completion(nil, error)
                }
            case .failure(let error):
                completion(nil, error)
            }
        }
    }
    
    /// Alamofire Rest Multipart API Request
    /// 타입 지정 데이터 처리 함수 
    /// validate().responseDecodable(of: APIModelValue.self)
    static func multipart_request1(image: UIImage, withName: String, parameters: Dictionary<String, Any>?, completion: @escaping (_ object: APIModelValue?, _ error: Error?) -> Void) {
        
        let headers: HTTPHeaders = [
            "Content-type": "multipart/form-data"
        ]
        
        AF.upload(multipartFormData: { multipartFormData in
            if let params = parameters {
                for (key, val) in params {
                    
                    if let temp = val as? String {
                        multipartFormData.append(temp.data(using: .utf8)!, withName: key)
                    }
                    
                    if let temp = val as? Int {
                        multipartFormData.append("\(temp)".data(using: .utf8)!, withName: key)
                    }
                }
            }

            if let imageData = image.pngData() {
                multipartFormData.append(imageData, withName: withName, mimeType: "image/png")
//                multipartFormData.append(fileUrl, withName: withName)
            }
            log(direction: .ETC, ofType: self, datas: "Param : \(String(describing: parameters))", "withName : \(withName)")
            
        }, to: url(), method: .post, headers: headers
        ).uploadProgress(queue: .main, closure: { progress in
            print("Upload Progress : \(progress.fractionCompleted)")
        }).validate().responseDecodable(of: APIModelValue.self) { response in
            switch response.result {
            case .success(let data):
                // obj 값은 자동으로 타입매칭 되서 넘어감.( APIModelValue.self)
                completion(data, nil)
            case .failure(let error):
                completion(nil, error)
            }
        }
    }
}

 

3. Model Class

  : 필요한 만금의 RestfulAPI Model 을 만들어서 사용함.

// Model Class 예제
// APIModel Protocol을 상속 받은 Model Class
struct TimeSyncModel: APIModelCodable {
    var rsltCD: Int?
    var rsltMsg: String?
    var rsltSet: RsltSet?
    
    enum CodingKeys: String, CodingKey {
        case rsltCD = "rslt_cd"
        case rsltMsg = "rslt_msg"
        case rsltSet = "rslt_set"
    }
    
    struct RsltSet: Codable {
        var current_timestamp: Double?
    }
}

 

4. ModelStatWrapper

   : @propertyWrapper 구조체로 ViewModel에서 Model의 상태값 전달하는 구조체

import Foundation

@propertyWrapper
struct ModelStateWrapper<Element> {
    let notiCenter = NotificationCenter()
    
    var wrappedValue: Element
    var binded: Bool = false
    /* 21.02.23 외부에서 호출 및 mutating funtion, value 사용
     1. is inaccessible due to 'private' protection level
     2. Cannot use mutating member on immutable value
     https://wlaxhrl.tistory.com/90
    */
    var projectedValue: ModelStateWrapper<Element> {
        get {
            return self
        } set {
            self = newValue
        }
    }
    
    mutating func accept(_ newValue: Element) {
        self.wrappedValue = newValue
        postValueChange(newVal: newValue)
    }
    
    mutating func bind( onNext: @escaping (Element) -> Void ) {
             
        if self.binded == false {
            notiCenter
                .addObserver(forName: NSNotification.Name("valueChange"),
                             object: nil,
                             queue: nil,
                             using: { noti in
                    let newElement = noti.userInfo?["newValue"] as! Element
                    onNext(newElement)
                    
                })
            self.binded = true
        }
    }
    
    func postValueChange(newVal: Element) {
        notiCenter
            .post(name: NSNotification.Name("valueChange"),
                  object: self,
                  userInfo: ["newValue":newVal])
    }
    
    func removeObserver() {
        notiCenter.removeObserver(self, name: NSNotification.Name("valueChange"), object: nil)
    }
}

 

5. ViewModel Class

   : Restful API 종류별로 ViewModel 클래스 생성 해서 사용

import Foundation
import Alamofire

/// APIRequest Protocol 상속 받은 ViewModel Class

class TimeSyncViewModel: APIRequest {
        
    typealias APIModelValue = TimeSyncModel
    static var method: Alamofire.HTTPMethod = .post
    static var suburl: API.WEB_SUBURL = .timesync
    
    @ModelStateWrapper  // <== 상태변경에 따른 bind되는 객체
    var timeSyncModel = APIModelValue()
    
    deinit {
        _timeSyncModel.removeObserver()
    }
    
    func request() {
        apiSerialQueue.async { [weak self] in
            Self.request(parameters: nil) { (object, error) in
                if let data = object {
                    self?._timeSyncModel.accept(data)
                } else {
                    if let error = error {
                        log(direction: .ERROR, ofType: self, datas: error.localizedDescription)
                    }
                }
            }
        }
    }
}

 

6. 사용 방법

class MainViewController: UIViewController {
	// Rest API 테스트용
    var timeSyncViewModel = TimeSyncViewModel()
    
    
    @IBAction func timeSyncRequestButton(_ sender: UIButton) {
    
    	//ViewModel 에서 request 후 ModelStateWrapper에 accept 시 bind 호출됨.
    	timeSyncViewModel.$timeSyncModel.bind { result in
            // result는 ViewModel에서 typealias 값으로 들어옴 (TimeSyncModel)
            // 데이터 처리...
        }
        
        // API 데이터 요청
        timeSyncViewModel.request()
    }
}

+ Recent posts