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!)×tamp=\(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()
}
}
'Swift > 기타' 카테고리의 다른 글
[SWIFT]ImageFileManager Class (0) | 2023.06.08 |
---|---|
[SWIFT]iCloud Drive에 파일 저장 (0) | 2023.06.08 |
[SWIFT]XCode 빌드번호 자동 증가 스크립트 (0) | 2023.06.01 |
[SWIFT]QR코드 처럼 코너 가이드 라인 그리기 (0) | 2023.05.24 |
[SWIFT]설치된 앱 아이콘 변경하기 (0) | 2023.05.17 |