[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를 잘 사용하기 위해 습득해야 할 요소들

  1. 문법과 용어 -> 책으로 해결 가능
  2. 개념을 체득(몸으로 익힘)하고, 잘 설계하기 -> 스스로 시행착오를 겪어야하고 시간이 많이 걸림. 책으로 배우기에는 한계가 존재

만약 Combine을 주로 쓰는 시기가 온다면

  1. 문법과 용어 -> RxSwift에서 많이 바뀐다
  2. 개념, 설계 -> 안 바뀜. 이러한 개념 부분들은 변할수가 없다.

따라서 지금 사용한다면, RxSwift를 배워도 크게 상관이 없는게, 개념과 설계는 변하지 않고, RxSwift가 포함되어 있는 레거시 코드를 유지보수를 해야 할 상황이 있을수도 있기 때문이다. 바뀔 문법과 용어는 금방 외울 수 있어서 크게 상관이 없기도 하다. 물론 개념과 설계를 제대로 익히지 못하고 Combine의 세상이 온다면 시간을 낭비하겠지만,,?


영어 변수명을 잘 지어보자 !

프로그램 = 글 !

Swift의 코드를 읽어보면 문장처럼 읽힌다 view.inserSubview(gradientView, at:2) 앞의 view가 주어, 뒤의 insertSubview가 동사 gradientView 목적어 at:2가 어떻게로 해석이 된다.

문장처럼 읽히게 하는 요소는 먼저 올바른 품사를 쓰는것이다.

동사의 변형

주의할 점은 동사는 변형한다는 부분이다.

동사원형과거형과거 분사
requestrequestedrequested
makemademade
hidehidhidden

먼저 동사 원형을 살펴보면

  • 함수 / 메서드에 사용
  • 조동사(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 타입은 복수로 명시하자.

불규칙 복수형

단수복수
viewviews
boxboxes
hashhashes
categorycategories
factoryfactories
halfhalves
childchildren
personpeople
indexindexes, indices
datumdata

이런 부분들은 사전을 찾으면 금방 알 수 있어서, 사전을 이용하면 좋다.

타입별 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

좋은 웹페이지 즐겨찾기