[TIL] 04.19

rbw·2022년 4월 19일
0

TIL

목록 보기
4/98
post-thumbnail

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

profile
hi there 👋

0개의 댓글