[TIL] 04.19
ViewModifier
코드의 겹치는 부분을 해결해주는 수정자라고 보면 될듯, 정의로는 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))
}
}
위와 같이 축약이 가능하다.
EnvironmentObject SwiftUI, 뷰간에 데이터 공유
앱의 뷰들끼리 데이턴를 공유해야 하는 경우 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로 수정한다.
ObservedObject
- String이나 Integer와 같은 간단한 로컬 프로퍼티 대신 외부 참조 타입을 사용한다는 점을 제외하면 @State와 매우 유사하다.
- @ObservedObject와 함께 사용하는 타입은 ObservableObject 프로토콜을 따라야 함
- @ObservedObject가 데이터가 변경되었음을 view에 알리는 방법은 여러가지이지만 가장 쉬운 방법은 @Published 프로퍼티 래퍼를 사용하는 것 => SwiftUI에 view reload를 트리거 함
타이머 사용예시
// 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)
}
}
Functional Reactive Programming
함수형 프로그래밍은 선언형 프로그래밍방법에 해당한다.
명령형 프로그래밍(Imperative programming, OOP) 선언형 프로그래밍(Declarative programming, FP)의 차이는 다음과 같다.
데이터를 정의하고, 변환 과정을 프로그래밍 할것인가, 행위를 정의 하고 그 안에 데이터를 넣을 것인가의 차이가 있다.
함수형 프로그래밍의 개념은 다음과 같다
- 데이터는 Immutalbe 하게 취급하자
- 데이터 변경이 필요시 새로 만들자
- Side-Effect를 없애기 위해서 Pure Function을 사용하자
- Function 들의 Composition과 High-Order Function 으로 프로그램을 만들자
- Data가 아닌 Process에 집중해서 프로그램을 만들자
서버에서 텍스트를 들고오는 함수 예시
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가 있다.
Reactive Programming 아이디어의 설명(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)
})
Reactive의 정의
- Async 한 처리를 Functional하게 처리하자
- 리턴값은 Stream인 Observable을 반홚자ㅏ
- Stream에 흐르는 Data/Event를 Opreator로 처리하자
- Stream과 Stream을 연결하자
- Data가 아닌 Process에 집중해서 프로그램을 만들자.
실제 사용되는 코드로 예시를 들어주시고 설명을 하셨는데, 코드의 가독성이 많이 올라간 모습을 볼 수 있었다. delegate패턴으로 작성을 하면 코드의 동작 과정을 읽을 떄 시선이 왔다갔다 하는데(분산 이 함수 실행하니까 위에거 실행하겠지 등 시선이 분산된다라는 뜻) 리액티브로 코드를 짠다면 코드를 읽을때는 위에서 아래로 순서대로 읽으면 된다. 실제 시간순으로 동작은 하지 않겠지만 읽을때는 순서대로 읽기 때문에 편하다는 느낌을 받았다.
한줄 요약
Functional : Side-Effect가 없도록 프로그래밍 하는 패러다임
Reactive : Async한 작업을 Functional 하게 처리하는 아이디어
RxSwift : Reactive 아이디어를 구현한 Swift 라이브러리
Combine, RxSwift 둘 중 어느것을 공부해야 할지 ?
Combine은 iOS 13 이상에서만 사용 가능하므로 iOS 13이 최소 지원버전이 되는 시점을 생각한다면 언제 상용화가 될지 얼추 예측이 가능하다. 2021년 4월 영상 기준 1년 후라고 하였으니 올해부터 회사들이 프로젝트에 사용할 수 있는 시점이 오고있다고 생각하면 되겠다
2022년 1월 11일 앱스토어의 조사 결과에 따르면, 지난 4년 동안 도입된 기기의 72%가 iOS15, 26%가 iOS14, 2%가 이전 버전으로 나와있다. 최소버전은 iOS 13으로 보면 될것같다.
Combine의 장점
- Apple이 만들고, OS에 기본 탑재(RxSwift는 별도 import 필요)
- SwiftUI와 밀접히 연동
- RxSwift의 장점을 거의 모두 가지고 있다.
- 부족한 점을 보완하는 오픈소스도 많이 나올것
나중에는 반드시 써야할지두
Rx, Reactive를 잘 사용하기 위해 습득해야 할 요소들
- 문법과 용어 -> 책으로 해결 가능
- 개념을 체득(몸으로 익힘)하고, 잘 설계하기 -> 스스로 시행착오를 겪어야하고 시간이 많이 걸림. 책으로 배우기에는 한계가 존재
만약 Combine을 주로 쓰는 시기가 온다면
- 문법과 용어 -> RxSwift에서 많이 바뀐다
- 개념, 설계 -> 안 바뀜. 이러한 개념 부분들은 변할수가 없다.
따라서 지금 사용한다면, RxSwift를 배워도 크게 상관이 없는게, 개념과 설계는 변하지 않고, RxSwift가 포함되어 있는 레거시 코드를 유지보수를 해야 할 상황이 있을수도 있기 때문이다. 바뀔 문법과 용어는 금방 외울 수 있어서 크게 상관이 없기도 하다. 물론 개념과 설계를 제대로 익히지 못하고 Combine의 세상이 온다면 시간을 낭비하겠지만,,?
영어 변수명을 잘 지어보자 !
프로그램 = 글 !
Swift의 코드를 읽어보면 문장처럼 읽힌다 view.inserSubview(gradientView, at:2)
앞의 view가 주어, 뒤의 insertSubview가 동사 gradientView 목적어 at:2가 어떻게로 해석이 된다.
문장처럼 읽히게 하는 요소는 먼저 올바른 품사를 쓰는것이다.
동사의 변형
주의할 점은 동사는 변형한다는 부분이다.
동사원형 | 과거형 | 과거 분사 |
---|---|---|
request | requested | requested |
make | made | made |
hide | hid | hidden |
먼저 동사 원형을 살펴보면
- 함수 / 메서드에 사용
- 조동사(can/should 등) 뒤에, e.g. canBecomeFirstResponder
- Life Cycle 관련 delegate, e.g. didREceive, willAppear, didComplete
과거형의 경우는 사실 쓸 일이 없다. 웬만하면 did를 사용하기 때문 따라서 과거형이 들어있는 변수명은 거의다 과거 분사라고 보면 된다. 과거 분사의 경우
- 과거 분사 = 형용사
- 수동의 의미 e.g. requestedData, hiddenView
- Bool 변수 e.g. isHidden, isSelected
Bool의 경우
의미 | 예시 | |
---|---|---|
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 |
이런 부분들은 사전을 찾으면 금방 알 수 있어서, 사전을 이용하면 좋다.
타입별 Naming Convention
URL 타입의 경우 전부 URL로 타입을 붙여줘야함, Size, Date, UIImage, Data 다 명시해줘야한다.
etc
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 로 늘려쓰려고 하는듯
아직 이 부분은 잘 이해가 안되서 나중에 한 번 찾아봐야 할듯 !
isHidden vs hidden
문법적으로는 문제가 없다만, 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 사용 x !
스위프트의 컨벤션은 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
결과를 바로 리턴하는 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
유저에게 요청하거나 작업이 실패할 수 있을 때 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
로 시작한다. 이로 보았을때, 실패할 수 있는 작업이거나 누군가가 요청을 거절 할 수 있을 때 사용하면 좋을것 같다.
perform/execute
작업의 단위가 클로져나 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
https://tv.naver.com/v/19397553
https://tv.naver.com/v/4980432/list/267189
Author And Source
이 문제에 관하여([TIL] 04.19), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@rbw/TIL-04.19저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)