코드의 겹치는 부분을 해결해주는 수정자라고 보면 될듯, 정의로는 View 또는 Modifer에 적용하여 원래 값의 다른 버전을 생성하는 수정자 라고 한다.
아래와 같이 겹치는 부분이 많은 텍스트들이 존재하는 코드를 줄여 줄 수 있다.
struct ContentView: View {
var body: some View {
VStack {
Text("hihi")
.fontWeight(.regular)
.font(Font.title)
.foregroundColor(Color.gray)
.padding(.all, 5)
Text("h i h i ?")
.fontWeight(.light)
.font(Font.title2)
.foregroundColor(Color.balck)
.padding(.all, 5)
Text("hi * 3")
.fontWeight(.bold)
.font(Font.caption)
.foregroundColor(Color.orange)
.padding(.all, 5)
}
}
}
// ViewModifier 사용시
struct customFont: ViewModifier {
var customWeight = Font.weight.regular
var customFont = Font.title
var customColor = Color.gray
func body(content: Content) -> some View {
content.font(customFont.weight(customWeight))
.foregroundColor(customColor)
.padding(.all, 5)
}
}
struct ContentView: View {
var body: some View {
VStack{
Text("안녕하세요")
.modifier(MyCutomFont())
Text("반갑습니다.")
.modifier(MyCutomFont(CustomWeight: .light, CustomFont: Font.title2, CustomColor: .black))
Text("Swift UI Modifier!")
.modifier(MyCutomFont(CustomWeight: .bold, CustomFont: Font.caption, CustomColor: .orange))
}
}
위와 같이 축약이 가능하다.
앱의 뷰들끼리 데이턴를 공유해야 하는 경우 SwiftUI 는 EnvironmentObject 속성 래퍼를 제공합니다. 이를 통해 필요한 곳 어디서나 모델 데이터를 공유 가능하고, 데이터가 변경될 때 뷰가 자동으로 업데이트 된 상태를 유지합니다.
그리고 위의 속성 래퍼를 사용하면 @ObservedObject를 사용하지 않아도 된다.
사용 예시
먼저 SceneDelegate.swift 파일 내부를 수정한다.
UserSetting을 추갛고, rootView 뒤에 EnvironmentObject를 추가해줘야 한다.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
// userSetting을 추가할 장소
let userSetting = UserSetting()
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(userSetting))
self.window = window
window.makeKeyAndVisible()
}
위에서 수정이 끝났다면, ContentView 내부에서 @ObservedObject 객체를 @EnvironmentObject로 대체해주면 된다.
그리고 @Binding으로 정의된 것들을 @EnvironmentObject로 수정한다.
타이머 사용예시
// MyTimer View
import SwiftUI
import Foundation
import Combine
class MyTimer: ObservableObject {
// ObservedObject 데이터가 변경되었음을 알리기 위해 @Published 래퍼를 사용한다.
@Published var value: Int = 0
init() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.value +=1
}
}
}
// ContentView
struct ContentView: View {
@ObservedObject var myTimer = MyTimer()
var body: some View {
Text("\(self.myTimer.value)")
.font(.largeTitle)
}
}
함수형 프로그래밍은 선언형 프로그래밍방법에 해당한다.
명령형 프로그래밍(Imperative programming, OOP) 선언형 프로그래밍(Declarative programming, FP)의 차이는 다음과 같다.
데이터를 정의하고, 변환 과정을 프로그래밍 할것인가, 행위를 정의 하고 그 안에 데이터를 넣을 것인가의 차이가 있다.
함수형 프로그래밍의 개념은 다음과 같다
서버에서 텍스트를 들고오는 함수 예시
import Foundation
// 문제는 수행되는데 시간이 걸린다. 서버를 갔다와야 하기 때문 ? 비동기적으로 처리해야함 밑의 함수는
// 수행을 막고 있다.
func getText1(from url: URL) -> String {
return try! String(contentsOf: url)
}
// 비동기적으로 만들었지만 리턴값을 언제 해야할지 애매해진다.
func getText2(from url: URL) -> String {
URLSession().dataTask(with: url) { (data, response, error) in
let text = String(data: data!, encoding: .utf8)
}.resume()
return ""
}
// 해결책 중 하나, 클로저를 받아서 콜백을 사용하는 해결책
func getText3(from url: URL, result: @escaping (String) -> Void) {
UURLSession().dataTask(with: url) { (data, response, error) in
let text = String(data: data!, encoding: .utf8)
result(text!)
}.resume()
}
// delegate를 사용하는 해결방식
var delegate: ((String) -> Void)? = nil
func getText4(from url: URL) {
URLSession().dataTask(with: url) { (data, response, error) in
let text = String(data: data!, encoding: .utf8)
delegate!(text!)
}.resume()
}
하지만 해결책들을 간단하게 getText1 처럼 만들려고 하는 고민이 있었고, 그 고민의 방법이 리액티브 프로그래밍이라고 부른다.
이는 async한 상황에서 그 데이터를 어떻게 처리할것인가에 대한 아이디어이다.
이는 아이디어이기 때문에, 이를 구현한 라이브러리(구글 검색으로, 리액티브 프로그래밍 라이브러리)가 많이 존재한다. 스위프트의 경우 RxSwift가 있다.
어떤 데이터를 생산해내는(Generator) 함수가 존재할것이고, 이 데이터를 받아서 처리하는(Consumer) 함수가 있고, 그 둘을 Stream으로 연결을 하고 생산해내는 함수를 Observable이라 부르고 데이터를 소비하는 함수를 Subscriber 라고 부른다. 이 흘러가는 흐름 내부에 Operator를 통해서 데이터를 변형하거나, 조작하거나 할 수 있다.
위의 코드로 예시를 들자면, getText의 리턴값으로 Stream을 주어서 바로 리턴한다. 그런 다음 서버로 가고, 데이터를 받아와서, 그 때 데이터가 생기면 스트림을 통해서 흘려 보낸다.
import RxSwift
func getText(from url: URL) -> Observable<String> {
// 바로 리턴을 한다
return Observable.create({ emitter in
URLSession().dataTask(with: url) { (data, response, error) in
guard error == nil else { return }
let text = String(data: data!, encoding: .utf8)!
// 스트림에 데이터를 보내는 모습
emitter.onNext(text)
emitter.onCompleted()
}
return Disposables.create()
})
}
getText(from: URL(string:"http://www.apple.com"!))
.subscribe(onNext: { text in
print(text)
})
실제 사용되는 코드로 예시를 들어주시고 설명을 하셨는데, 코드의 가독성이 많이 올라간 모습을 볼 수 있었다. delegate패턴으로 작성을 하면 코드의 동작 과정을 읽을 떄 시선이 왔다갔다 하는데(분산 이 함수 실행하니까 위에거 실행하겠지 등 시선이 분산된다라는 뜻) 리액티브로 코드를 짠다면 코드를 읽을때는 위에서 아래로 순서대로 읽으면 된다. 실제 시간순으로 동작은 하지 않겠지만 읽을때는 순서대로 읽기 때문에 편하다는 느낌을 받았다.
한줄 요약
Functional : Side-Effect가 없도록 프로그래밍 하는 패러다임
Reactive : Async한 작업을 Functional 하게 처리하는 아이디어
RxSwift : Reactive 아이디어를 구현한 Swift 라이브러리
Combine은 iOS 13 이상에서만 사용 가능하므로 iOS 13이 최소 지원버전이 되는 시점을 생각한다면 언제 상용화가 될지 얼추 예측이 가능하다. 2021년 4월 영상 기준 1년 후라고 하였으니 올해부터 회사들이 프로젝트에 사용할 수 있는 시점이 오고있다고 생각하면 되겠다
2022년 1월 11일 앱스토어의 조사 결과에 따르면, 지난 4년 동안 도입된 기기의 72%가 iOS15, 26%가 iOS14, 2%가 이전 버전으로 나와있다. 최소버전은 iOS 13으로 보면 될것같다.
나중에는 반드시 써야할지두
만약 Combine을 주로 쓰는 시기가 온다면
따라서 지금 사용한다면, RxSwift를 배워도 크게 상관이 없는게, 개념과 설계는 변하지 않고, RxSwift가 포함되어 있는 레거시 코드를 유지보수를 해야 할 상황이 있을수도 있기 때문이다. 바뀔 문법과 용어는 금방 외울 수 있어서 크게 상관이 없기도 하다. 물론 개념과 설계를 제대로 익히지 못하고 Combine의 세상이 온다면 시간을 낭비하겠지만,,?
프로그램 = 글 !
Swift의 코드를 읽어보면 문장처럼 읽힌다 view.inserSubview(gradientView, at:2)
앞의 view가 주어, 뒤의 insertSubview가 동사 gradientView 목적어 at:2가 어떻게로 해석이 된다.
문장처럼 읽히게 하는 요소는 먼저 올바른 품사를 쓰는것이다.
주의할 점은 동사는 변형한다는 부분이다.
동사원형 | 과거형 | 과거 분사 |
---|---|---|
request | requested | requested |
make | made | made |
hide | hid | hidden |
먼저 동사 원형을 살펴보면
과거형의 경우는 사실 쓸 일이 없다. 웬만하면 did를 사용하기 때문 따라서 과거형이 들어있는 변수명은 거의다 과거 분사라고 보면 된다. 과거 분사의 경우
의미 | 예시 | |
---|---|---|
is + 명사 | ~ 인가? | isDescandant(of:), isVideo, isFavorite |
is + 현재진행형(~ing) | ~ 하는 중인가? | isExecuting, isPending |
is + 형용사 | ~ 인가 ? ~ 되었는가 ? | isOpaque, isEditable isSelected, isHidden |
can/should/will + 동사원형 | ~ 할 수 있나?, ~ 해야하나? ~ 할 것인가? | canBecomeFirstResponder |
has + 명사 | ~ 을 가지고 있는가? | hasVideo, hasiCloudAccount |
has + 과거분사 | ~ 되었는가? (상태의 지속 강조) | hasConnected, hasEndend |
동사원형 용법 | - | preservesSuperviewLayoutMargins |
is + 동사원형 | 흔히 하는 실수 | isAuthorize, isDelete, isFind, isAdd |
let album: Album
let albums: [Album]
for album in albums {
}
하나의 객체, 인스턴스는 단수로, array 타입은 복수로 명시하자.
단수 | 복수 |
---|---|
view | views |
box | boxes |
hash | hashes |
category | categories |
factory | factories |
half | halves |
child | children |
person | people |
index | indexes, indices |
datum | data |
이런 부분들은 사전을 찾으면 금방 알 수 있어서, 사전을 이용하면 좋다.
URL 타입의 경우 전부 URL로 타입을 붙여줘야함, Size, Date, UIImage, Data 다 명시해줘야한다.
ID vs id vs identifier
apple 문서의 예시
var identifier: AVMetadataIdentifier? { get } // AVMetadataItem
var localIdentifier: String { get } // PHObject
var identifier: String { get } // CLRegion
var identifier: String { get } // MLMediaGroup, SCNReferenceNode,...
var objectID: NSManagedObjectID { get } // NSManagedObjectID
var recordID: CKRecordID { get } // CKRecord
var uniqueID: String? { get } // AVMetadataGroup ,, 예외 ?
전부 다 규칙을 지키지는 않는것 같다... 그래도 id 타입 자체를 커스텀으로 만든다면 ID 대문자를 붙이는 경향이 보인다. 그리고 대부분 identifier 로 늘려쓰려고 하는듯
아직 이 부분은 잘 이해가 안되서 나중에 한 번 찾아봐야 할듯 !
문법적으로는 문제가 없다만, swift에 컨벤션은 is를 붙이는게 맞는것 같다고 말씀하심.
구조체에서 사용하는 프로퍼티의 중복을 줄여야한다.
struct User {
let userID: String
}
let id = user.userID
struct ImageDownloader {
func downloadImage(from url: URL) {}
}
let imageDownloader = ImageDownloader()
imageDownloader.downloadImage(from: imageURL)
// 중복 제거 코드
struct User {
let identifier: String
}
let id = user.identifier
imageDownloader.download(from: imageURL)
imageDownloader.fetch(from: imageURL)
imageManager.download(from: imageURL)
스위프트의 컨벤션은 get을 쓰지 않는다.
func date(from string: String) -> Date?
func anchor(for node: SCNNode) -> ARAnchor?
func distance(from location: CLLocation) -> CLLocationDistance
func track(withTrackID trackID: CMPersistentTrackID) -> AVAssetTrack?
코드를 작성하면 데이터를 어디선가 가져오는 함수를 자주 작성하게 되는데, 디스크에 저장되어 있는 이미지, 리모트 서버에 있는 유저 DB, 메모리 캐싱 데이터 등등 이런 메서드의 의미들로 fetch, request, perform
을 사용하는데 좀 더 디테일한 차이를 알아보겠다
결과를 바로 리턴하는 fetch
//PHAsset - Photos Framework
class func fetchAssets(withLocalIdentifiers identifiers: [String], options: PHFetchOptions?) -> PHFetchResult<PHAsset>
//PHAssetCollection - Photos Framework
class func fetchAssets(in assetCollection: PHAssetCollection, options: PHFetchOptions?) -> PHFetchResult<PHAsset>
//NSManagedObjectContext - Core Data
func fetch<T>(_ request: NSFetchRequest<T>) throws -> [T] where T : NSFetchRequestResult
fetch
는 결과물을 바로 리턴한다. 오래 걸리지 않는 동기적 작업임을 뜻한다.
유저에게 요청하거나 작업이 실패할 수 있을 때 request
//PHImageManager
func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID
//PHAssetResourceManager
func requestData(for resource: PHAssetResource, options: PHAssetResourceRequestOptions?, dataReceivedHandler handler: @escaping (Data) -> Void, completionHandler: @escaping (Error?) -> Void) -> PHAssetResourceDataRequestID
//CLLocationManager
func requestAlwaysAuthorization()
func requestLocation()
//MLMediaLibrary
class func requestAuthorization(_ handler: @escaping (MPMediaLibraryAuthorizationStatus) -> Void)
반면에 request
를 쓰는 함수들은 비동기 작업이라 handler를 받거나 delegate 콜백으로 결과를 전달한다. 그리고 실패유무도 알수 있다. 또한 유저에게 특정 권한을 요청하는 메서드는 모두 request
로 시작한다. 이로 보았을때, 실패할 수 있는 작업이거나 누군가가 요청을 거절 할 수 있을 때 사용하면 좋을것 같다.
작업의 단위가 클로져나 Request로 래핑되어 있다면 perform, execute
//VNImageRequestHandler
func perform(_ requests: [VNRequest]) throws
//PHAssetResourceManager
func performChanges(_ changeBlock: @escaping () -> Void, completionHandler: ((Bool, Error?) -> Void)? = nil)
//NSManagedObjectContext
func perform(_ block: @escaping () -> Void)
//CNContactStore
func execute(_ saveRequest: CNSaveRequest) throws
//NSFetchRequest
func execute() throws -> [ResultType]
perform, execute
는 파라미터로 request 객체나 클로져를 받는 경우이다.
변수명을 잘 표현하는것도 매우 중요하다고 생각한다. 남이 읽을때 편하게 읽을 수 있을거라고 생각하기 때문이다. 영어 변수명을 짓기전에 먼저 동의어 사전을 활용해보자 그리고 좋은 코드들을 많이 읽기 !
참조
https://seons-dev.tistory.com/78
https://seons-dev.tistory.com/80