2024년 04월 06일

오늘 배운 내용

  • 객체지향, 함수형 프로그래밍의 개념 이해
  • Swift 함수의 정의와 호출
  • 오버로딩, 오버라이딩

오늘 푼 문제

  • 짝수와 홀수 (프로그래머스)

오늘은 Swift 함수와 더불어 Swift의 언어적 특성에 대해서 기초부터 다시 공부하기 시작했다.

ChatGPT에 너무 의존해서는 안된다. ChatGPT가 알려주는 정보가 틀릴 수 있기 때문이다.

나는 그 틀린정보를 구분할 수 있는 능력이 필요하고, 어떤 질문을 해야 좋을지 그 ‘바탕’이 되어줄 것이 필요하다. 좋은 코드를 짜기 위해서는 기본부터 다시 시작해서 배울 필요가 있다고 생각한다.


2024년 04월 07일

오늘 배운 내용

  • 흐름 제어 (조건문, 반복문)

오늘 푼 문제

  • 평균 구하기 (프로그래머스)
  • 두 개 뽑아서 더하기 (프로그래머스)

느낀점 오늘 Swift로 문제를 풀면서 이중 반복문을 구현할 때 조금 헷갈렸다. 스위프트 프로그래밍 책을 다시 보면서 반복문, 흐름 제어 관련해서 다시 한번 꼼꼼히 공부하였다!


2024년 04월 08일

오늘 배운 내용

  • @State: SwiftUI는 @State 를 통해 UI 컴포넌트(toggle, slider, stepper…)의 상태를 관리, 변경사항에 따라 UI를 동적으로 업데이트 가능하다!
  • .onChange: 수정자를 통해 세그먼트 컨트롤의 변경 사항에 따라 추가 코드를 실행할 수 있다!
  • 그 외, padding, spacer, offset 등 기존에 SwiftUI 프로젝트를 하면서 충분히 익숙해진 것들을 다뤘다.

오늘 푼 문제

  • 로또의 최고 순위와 최저 순위

느낀점 오늘 SwiftUI 를 다시 한번 흩어보면서 몰랐던 부분이 있는지 체크 하였다. UI 컴포넌트를 view 에 배치하고, Modifier 를 추가할 때, 코드에서도 확인할 수 있지만 inspector 창에서도 상세하게 확인할 수 있음을 새롭게 알 수 있었다! 그러나 아직 잘 활용할 것 같지는 않아보인다.

오늘 코테 문제를 통해서, 조건문과 반복문, switch 구문을 swift 에서 어떻게 쓸 수 있을지 연습해볼 수 있었다. 자주 사용하는 기초적인 문법이므로 계속해서 연습할 필요가 있다.

해당 문제는 아이디어는 쉽지만 filter 메서드를 몰랐다면 코드가 꽤 복잡해진다. filter는 조건이 true인 모든 요소들 (lottos에서 win_nums에 포함된 번호들)로 구성된 새 배열을 반환하는 메서드! 라는 것을 기억하며 오늘 TIL 을 마친다!


2024년 04월 09일

오늘 배운 내용

  • alert(_:isPresented:presenting:actions:message:)
  • confirmationDialog(_:isPresented:titleVisibility:presenting:actions:message:)
  • contextMenu 와 preview
  • onTapGesture

오늘 푼 문제

  • 로또의 최고 순위와 최저 순위

느낀점 오늘도 SwiftUI의 UI Components를 흩어보았다. 다시 볼수록 SwiftUI 가 선언형 프로그래밍을 위해서 만들어진 프레임워크임을 깨닫는다. 코드가 보기 쉽고 유연하게 view 를 구성하기 위해서 계속해서 발전하고 있다고 생각한다.

deprecated 된 코드 중 alert 는 인상 깊었다.

기존 코드는 alert 안에 Alert 객체가 있는 모습인데 부자연스럽다. 이에 반해 변경된 코드는 Alert 안의 요소들을 좀 더 선명하게 분리하였고, 버튼에 대한 역할(role) 을 명시하는 등 코드에 유연성을 증가시켰다고 볼 수 있다.

SwiftUI 는 선언적 UI프레임워크로서 계속 변화하고 있기에, 내 코드도 계속해서 업데이트 해야 한다. 따라서 바뀌고 있는 코드를 외우는 것이 아니라, 공식문서를 보고 빠르고 정확하게 파악하여 코드에 적용하는 데 집중하여야 한다.

또한 코테 문제를 풀면서 나는 아직 클로저와 map 과 같은 여러가지 메서드를 사용하는 것이 익숙치 않다는 것을 깨달았다. 이것에 익숙해져야 Swift 다운 코드로 문제를 풀 수 있다.


2024년 04월 10일

오늘 푼 문제

  • k번째 수

느낀점 오늘은 ArraySlice 에 대해서 간단히 살펴보았다.

위 문제와 같이 인덱스 범위를 통해 새로운 범위의 배열을 지정한 경우에는, Array 타입이 아닌 ArraySlice 타입이 된다.

사실 이 문제는, 파이썬으로 이전에 풀어보았는데, 스위프트에서는 배열의 인덱스 슬라이딩을 하는 과정에서 Array로 타입변환을 해주지 않으면, 리턴 값이 달라 에러가 나기에 주의 해야했다.


2024년 04월 11일

오늘 배운 내용

  • NavigationStack
  • Grid
  • Animation
  • offsetBy (시작 지점부터 떨어진 정수 값만큼을 더한 위치를 반환)

오늘 푼 문제

  • 가운데 글자 가져오기 Swift 에서는 문자열 인덱싱을 바로 하지 않기에, 문자열을 배열로 먼저 바꾸는 것이 팁.

느낀점

NavigationView 에서 NavigationStack 으로 바뀌었는데, 뷰 전환은 앱 내에서 많이 이루어지는 요소인만큼, 확실하게 알고 가는 것이 중요하다. NavigationLink 를 추가할 때, 동시에 navigationDestination(for:destination:) 모디파이어를 추가하여 뷰를 데이터 유형과 연결할 수 있다는 것이 특징이다.


2024년 04월 12일

오늘 배운 내용

  • GeometryReader: 다양한 iOS 화면 크기에 적응하는 여러 뷰를 포함할 수 있는 독특한 컨테이너. 사용자 인터페이스 항목을 ‘정밀하게 배치할 필요가 있을 때’ 사용한다.

오늘 실습 프로젝트

  • Timer (macOS) 사운드 mute 기능을 추가하기 위해, AVFoundation 을 가지고 사운드를 재생하는 SoundManager 클래스에 ObservableObject 를 추가함으로써, 다른 뷰에서도 해당 인스턴스에 접근이 가능하도록 만들었다. 그랬더니 알람 사운드를 사용자가 자유롭게 설정할 수 있게 하게 되었다.

오늘 푼 문제

  • 2016년 (프로그래머스)

느낀점 오늘은 SwiftUI 를 이용하여, MacOS 에서 사용할 수 있는 timer 앱을 만들었다. 타이머는 Timer.publish(every:on:in:).autoconnect()를 사용하여, 매초마다 시간을 업데이트하는 방식으로 타이머와 애니메이션을 구현하였다. 나름 여러가지 애니메이션, 기능들을 넣었다고 생각했는데 다른 팀원분들도 애니메이션, 디자인, 그리고 기능까지 잘 만드는 분들이 많았다. 그 분들과 함께 협업 하면서 계속 성장할 수 있을 것이다.

앞으로 더 공부할 점

  • 반응형 프로그래밍
  • git(pull request, merge …)

2024년 04월 13일

오늘 배운 내용

  1. 배열의 내림차순 정렬방법
    • array.sort(by: > )
    • array.sort { $0 > $1 }

  1. stride(from:to:by:)

오늘 푼 문제

  • 과일 장수 (프로그래머스)

느낀점 오늘의 문제는 간단하게 풀 것이라고 생각했는데, 생각보다 잘 풀리지 않아서 생각을 계속했다. 덕분에 이제 이 정도 난이도의 문제는 좀 더 자신감 있게 다가갈 수 있게 된 것 같다.

앞으로도 하나의 문제를 풀 때, 최대한 많은 고민과 생각을 해서 얻어가도록 하고, 마지막에 다른 사람의 코드를 보면서 새로운 것을 배우는 방식으로 공부해야겠다.


2024년 04월 14일

오늘 배운 내용

  1. 문자열을 반복(iterate)할 때 각 반복에서 처리되는 단위는 Character

  2. Swift에서 Int 생성자는 String을 입력으로 받을 수 있지만, Character를 직접 받을 수 없다. 그러므로 Character를 먼저 String으로 변환한 다음 Int로 변환해야 한다.
    이 때, wholeNumberValue 프로퍼티도 사용 가능.

  3. reduce 복습

func reduce<Result>(
    _ initialResult: Result,
    _ nextPartialResult: (Result, Self.Element) throws -> Result
) rethrows -> Result

reduce 메소드는 컬렉션에 있는 모든 요소를 하나의 값으로 결합하기 위해 사용된다.

이 메소드는 컬렉션의 요소를 순서대로 처리하면서, 각 요소에 대해 사용자가 제공한 결합 작업(Closure)을 수행한다.

즉, reduce는 컬렉션의 모든 요소를 축소(reduce) 하여 단일 값으로 만드는 것이다.


  • Result 는 reduce 연산 결과의 타입이다. (제네릭)
  • initialResult 는 연산의 시작점에 사용되는 초기값!
  • nextPartialResult 는 Closure 로서, 누적된 결과 (Result) 와 현재 처리중인 요소(Element) 를 받아 다음 누적 결과를 반환한다.


오늘 푼 문제

  • 자릿수 더하기 (프로그래머스)



느낀점 오늘의 문제는 간단하게 풀 것이라고 생각했는데, 생각보다 잘 풀리지 않아서 생각을 계속했다. 덕분에 이제 이 정도 난이도의 문제는 좀 더 자신감 있게 다가갈 수 있게 된 것 같다.

앞으로도 하나의 문제를 풀 때, 최대한 많은 고민과 생각을 해서 얻어가도록 하고, 마지막에 다른 사람의 코드를 보면서 새로운 것을 배우는 방식으로 공부해야겠다.


2024년 04월 15일

오늘 배운 내용 함수를 짤 때, 함수 의존성을 주의해야 한다. 클린코드 중 하나.
- 함수 안의 함수를 호출하는 것은 복잡도를 높일 수 있다. - 다시 말해, 함수를 기능별로 잘 분리해서 사용해야 한다는 것.

// view ...

                Button(action: {
                    selectWrong()
                    // loadGame()
                }) 
                //.... 생략

// func ...
    func selectCorrect() {
        if resultNumber == number1 * number2 {
            countCorrect += 1
        } else {
            countWrong += 1
        }
        loadGame()
    }

위 swiftUI 코드에서 처럼, selectCorrect 안에서 loadGame 을 호출하기 보다, view 안의 Button action 클로저에서 따로 따로 기능별로 함수를 호출하는 것이 좋다.

selectcCorrect 함수는 이름 그대로 점수를 세는 기능 자체만 담고 있어야 한다, 만약 loadGame() 기능을 담으면 지저분한 코드가 되는 것이다.



오늘 푼 문제

  • 제일 작은 수 제거하기 (프로그래머스)



느낀점 compactMap 을 사용한 마지막 풀이의 경우, 사실 이 부분을 완전히 이해하지 못했다. 아직 이런 고차함수를 문제에 바로 적용하기는 힘들지만, 그래도 지금 한 번 정리한다면 다음에 볼 때는 지금보다 나을 것이다.


2024년 04월 16일

오늘 배운 내용

  1. class VS struct

struct 는 값 타입이다. 이것은 struct의 인스턴스를 다른 변수에 할당하거나 함수에 전달할 때, 해당 인스턴스의 복사본이 생성되고 전달된다는 것을 의미한다.

myStruct1을 myStruct2에 할당한 후 myStruct2의 name을 변경하면, myStruct1의 name은 변경되지 않는다. 이는 두 구조체 인스턴스가 서로 독립적인 복사본이기 때문이다.

class 는 참조 타입이다. 클래스의 인스턴스를 다른 변수에 할당하거나 함수에 전달할 때, 인스턴스 자체가 아닌 그 인스턴스를 가리키는 참조가 전달된다.

myClass1을 myClass2에 할당한 후 myClass2의 name을 변경하면, myClass1의 name도 변경된다. 이는 myClass1과 myClass2가 동일한 인스턴스를 가리키기 때문이다.

class 는 상속을 지원한다. 다른 클래스 로부터 속성과 메소드를 상속받아 사용 가능하다.

struct 는 상속을 지원하지 않는다. 그러나 프로토콜을 통해 유사하게 사용 가능하다.


클래스: 클래스 인스턴스는 deinit을 사용하여 할당 해제될 때 정리할 수 있는 코드를 실행할 수 있습니다.


구조체: 구조체에는 할당 해제 시점에 호출되는 deinit 메소드가 없습니다.

클래스: 클래스 인스턴스는 실행 시간에 타입을 검사하고 해석하기 위해 타입 캐스팅을 사용할 수 있습니다. 이는 인스턴스가 그것의 클래스나 슈퍼클래스 중 하나의 인스턴스인지 확인하는데 유용합니다.


구조체: 구조체는 타입 캐스팅을 지원하지 않습니다.

클래스: 복잡한 객체 모델을 구현할 때, 상속을 통해 코드 재사용성이 필요할 때, 또는 애플리케이션 내에서 인스턴스의 상태를 여러 곳에서 공유해야 할 때 사용됩니다.

구조체: 비교적 작고, 단순한 데이터 값을 캡슐화할 때 주로 사용됩니다. Swift의 많은 기본 타입들(예: Int, String, Array)은 실제로 구조체로 구현되어 있습니다.


Swift는 구조체 사용을 선호하며, Apple은 클래스보다 구조체를 사용하도록 권장합니다.


이는 구조체가 더 예측 가능하고, 관리하기 쉬운 코드를 만드는데 도움이 되기 때문입니다. 그러나 상속이 필요하거나, 데이터를 여러 곳에서 공유해야 할 때는 클래스가 더 적합할 수 있습니다.

struct SampleStruct {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func buildHelloMsg() {
        "Hello " + name
    }
}

class SampleClass {
    var name: String
    init(name: String) {
        self.name = name
    }
    func buildHelloMsg() {
        "Hello " + name
    }
}

let myStruct1 = SampleStruct(name: "Mark")
var myStruct2 = myStruct1
myStruct2.name = "David"

print(myStruct1.name)
print(myStruct2.name)

let myClass1 = SampleClass(name: "Mark")
var myClass2 = myClass1
myClass2.name = "David"

print(myClass1.name)
print(myClass2.name)



  1. property wrapper

Swift의 Property Wrapper는 속성에 저장되는 값에 대한 관리를 캡슐화하고 재사용하기 위한 강력한 도구입니다. 이를 통해 값의 검증, 변형, 저장 방식 등을 커스터마이즈할 수 있으며, 이러한 로직을 속성 선언에 직접 적용하지 않고도 사용할 수 있습니다. Property Wrapper는 @propertyWrapper 키워드로 정의됩니다.

Property Wrapper의 주요 기능: 값의 저장: Property Wrapper는 속성에 할당된 값의 저장 방식을 커스터마이즈할 수 있게 해줍니다. 값의 변형과 검증: 할당되기 전이나 조회되기 전에 값을 변형하거나 검증할 수 있습니다. 코드 재사용성과 관심사의 분리: 공통적인 속성 관리 코드를 여러 속성에 재사용할 수 있습니다.


@propertyWrapper
 struct MinMaxVal<V: Comparable> {
     var value: V
     let min: V
     let max: V

     init(wrappedValue: V, min: V, max: V) {
         value = wrappedValue
         self.min = min
         self.max = max
     }

     var wrappedValue: V {
         get { return value }
         set {
             if newValue > max {
                 value = max
             } else if newValue < min {
                 value = min
             } else {
                 value = newValue
             }

         }
     }
 }

 struct Demo {
     @MinMaxVal(min: 10, max:150) var value: Int = 100
     @MinMaxVal(min: "Apple", max: "Orange") var value2: String = ""
 }

 var demo = Demo()

 demo.value2 = "Banana"

 print(demo.value2)

 demo.value2 = "Pear"

 print(demo.value2)


오늘의 실습 프로젝트

  1. class (init, super init 사용)

    • 클래스를 초기화할 때, 부모 클래스에서 받은 프로퍼티는 super init 으로 초기화 한다
    • 부모 클래스를 상속하여 하위 클래스를 여러 개 만들고, 데이터의 타입을 부모 클래스 타입으로 하면, 하위 클래스 모두 호환 가능
  2. Section - header

    • swiftUI 에서 Section(header: Text(“Electric Cars”)) 와 같이, view 에서 분류 가능
    • 이어서, ForEach(cars.filter { $0 is ElectricCar } 에서, filter 와 같은 메서드를 사용하여 원하는 데이터만 필터링

오늘 푼 문제

  • 약수의 합 (프로그래머스)



느낀점

오늘은 데이터 모델링 실습을 하면서, 클래스 상속, 재정의를 몸소 학습할 수 있었다. 개념으로만 알고 있던 내용을 실습을 통해 적용하면서 앞으로 이렇게 부지런히 개념을 터득해나가면 되겠다는 생각이 들었다.

무엇보다도 프로토콜 지향 프로그래밍의 매력에 흠뻑 빠져들었다. 프로토콜 익스텐션에 메서드를 추가함으로써, 해당 프로토콜을 준수하는 클래스의 하위 클래스에 모두 적용이 가능하다! 이를 통해 클린 아키텍쳐를 구현할 수 있다!!


2024년 04월 17일

오늘 배운 내용

에러 핸들링?

1. throwing: iOS 앱의 메서드 내에서 **원하는 결과가 나오지 않을 경우에 에러를 발생(throwing)** 시키는 것

2. 메서드가 던진 (throwing) 에러를 잡아서 처리하는 것

Error 프로토콜을 따르는 열거형으로 에러 타입을 선언한다.

enum FileransferError: Error {
    case noConnection
    case lowBandwidth
    case fileNotFound
}

에러 던지기 (throws)

let connectionOK = true
let connectionSpeed = 30.00
let fileFound = false

enum FileTransferError: Error {
    case noConnection
    case lowBandwidth
    case fileNotFound
}

func fileTransfer() throws {
    guard connectionOK else {
        throw FileTransferError.noConnection
    }

     guard connectionSpeed > 30 else {
         throw FileTransferError.lowBandwidth
     }

     guard fileFound else {
         throw FileTransferError.fileNotFound
     }
     // guard 구문을 통해 각 조건이 참인지 거짓인지를 검사한다. 만약 거짓이라면 throw 구문을 통해, enum 열거형에 있는 에러 값들 중 하나를 던지는 것이다.
 }

 func sendFile() -> String {
     do {
        // 메서드, 함수가 에러를 던지도록 선언했다면, try 구문을 앞에 붙여서 호출한다.
        // do-catch 구문으로 던져진 모든 에러를 검사한다
         try fileTransfer()
     } catch FileTransferError.noConnection {
         return "No Connection"
     } catch FileTransferError.lowBandwidth {
         return "Speed too low"
     } catch FileTransferError.fileNotFound {
         return "File Not Found"
     } catch {
         return "Unknown error"
     }
     return "Successful transfer"
 }

defer 메서드가 결과를 반환하기 직전에 실행되어야 하는 일련의 코드를 지정할 수 있게 함. -> 함수를 종료하기 직전에 정리해야 하는 변수나 상수를 처리하는 용도, “정리” 작업

func sendFile() -> String {

     defer {
         // 파일 닫기 등 종료 준비
         // defer 블록은 함수의 가장 마지막에 실행된다. (단 defer가 return 위에 있을 때)
         print("end of sendFile")
     }

     do {
         try fileTransfer()
     } catch FileTransferError.noConnection, FileTransferError.lowBandwidth {
         return "Connection Problems"
     } catch FileTransferError.fileNotFound {
         return "File Not Found"
     } catch let error {
         return "Unknown error"
     }
     return "Successful transfer"
 }

SwiftUI 아키텍쳐

  • App
  • Scene
  • View

2024년 04월 18일

오늘 배운 내용

  • 상태 프로퍼티 @State

    • 상태 프로퍼티 값이 변경되면, 그 프로퍼티가 선언된 뷰 계층 구조를 다시 렌더링한다.

    • 텍스트 필드나 토글 위에서, 상태 프로퍼티를 바인딩($) 함으로써, 새로운 값으로 상태 프로퍼티를 자동으로 뷰에 업데이트 하는 방식.

      • 상태가 변하면 레이아웃에 있는 다른 뷰들도 같이 업데이트 된다.
      TextField("Enter user name", text: $userName)
      Text(userName)      // userName 이 업데이트 되면 같이 업데이트됨
  • 상위뷰에서 선언한 상태 프로퍼티에 대해 접근해야 하는 경우, @Binding


2024년 04월 19일

오늘 배운 내용

  • Binding

  • Observable 객체

  • 비동기 처리

    • 프로세스, 스레드에 대한 이해
    • async/await
    • MainActor
  • ScenePhase

  • @StateObject, @ObservedObject, @EnvironmentObject

  • ViewModel, MVVM 패턴


프로세스, 스레드에 대한 이해

스레드는 프로세스 내에서 실행되는 실행 단위입니다. 프로세스는 실행 중인 프로그램의 인스턴스로, 자신만의 메모리 공간을 가집니다. 반면, 스레드는 프로세스 내의 경량 프로세스라고 할 수 있으며, 같은 프로세스 내의 다른 스레드와 메모리(힙 공간)를 공유합니다.

프로그램이 실행되면 기본적으로 하나의 “메인 스레드”에서 작업을 처리합니다. 그러나 무거운 작업을 메인 스레드에서 실행하면 애플리케이션의 반응성이 떨어질 수 있습니다(예: UI가 멈추는 현상). 이를 방지하기 위해 비동기 처리를 사용하여 백그라운드 스레드에서 무거운 작업을 수행하고, 작업 완료 후 메인 스레드로 결과를 전달할 수 있습니다.

Swift에서 async와 await는 비동기 프로그래밍을 위한 키워드로, 복잡한 비동기 코드를 동기 코드처럼 간결하고 이해하기 쉽게 작성할 수 있게 해줍니다. 이 기능은 Swift 5.5 이상에서 사용할 수 있으며, Swift의 Concurrency 모델의 핵심 부분입니다.

async

async 키워드는 함수가 비동기적으로 실행될 수 있음을 나타냅니다. async 함수 내부에서는 시간이 걸릴 수 있는 작업을 실행하고, 그 결과를 기다릴 수 있습니다. 이런 함수들은 보통 네트워크 요청, 파일 입출력, 또는 사용자 입력과 같이 프로그램의 나머지 실행 흐름을 차단하지 않아도 되는 작업을 수행합니다.
-> async 를 씀으로써, 해당 구문은 비동기적으로 실행하겠다고 선언하는 것.


await

await 키워드는 async 함수 내부에서 사용되며, 비동기 함수의 결과가 준비될 때까지 기다리게 합니다. await는 해당 지점에서 실행을 일시 중지하고, 작업이 완료될 때까지 다른 작업을 수행할 수 있는 기회를 제공합니다. 작업이 완료되면, 함수는 중지된 지점부터 실행을 재개합니다.
-> await 를 씀으로써, 호출된 함수의 반환값이 올 때까지 기다리는 것.


async-let 바인딩 예제

비동기 함수가 백그라운드에서 실행되는 동안 호출하는 함수 내에서 코드를 계속 실행 하는 것.
-> async-let 바인딩을 사용하여 해당 코드에서 나중까지 기다리는 것을 지연시킴.

func takesTooLong() async -> Date {
    sleep(5)
    return Date()
}

func doSomething() async {
    print("Start \(Date())")
    async let result = takesTooLong()
    print("After async-let \(Date())")
    
    // 비동기 함수와 동시에 실행할 추가 코드
    print("result = \(await result)")
    print("End \(Date())")
}


/*
Start 2024-04-19 13:30:59 +0000
After async-let 2024-04-19 13:30:59 +0000
result = 2024-04-19 13:31:04 +0000
End 2024-04-19 13:31:04 +0000
*/

takesTooLong() 함수가 async let을 통해 비동기적으로 실행된다. 이 함수는 5초 동안 슬립(sleep)한 후 현재 날짜를 반환하는데 처음에 await가 사용되지 않았으므로, 실행 흐름은 바로 다음 줄로 넘어간다.

이 시점에서는 takesTooLong() 함수의 실행이 아직 완료되지 않았으므로, 이 메시지는 takesTooLong() 함수가 반환하는 시각보다 약 5초 빠르다.

-> async let을 사용하여 비동기 작업을 시작하고, 작업의 결과가 필요한 시점에서 await를 사용하여 그 결과를 기다리는 패턴


data race (데이터 경쟁)

여러 작업이 동일한 데이터를 동시에 접근할 때 문제가 발생한다.
-> 데이터 손상, 충돌 등의 문제 발생

actor (액터)

이러한 data race를 해결하는 방법이 바로 actor 이다.

(액터는 Swift의 동시성 모델에서 상태(데이터)를 안전하게 캡슐화하고 관리하는 역할을 합니다. 액터 내부의 상태는 기본적으로 스레드 안전하며, 액터의 메소드는 동시에 여러 스레드에서 호출되더라도 해당 상태를 안전하게 접근할 수 있게 해줍니다.)

class 대신에 actor 를 선언하면 된다.

단, actor 는 Task 클로저, 비동기 함수와 같은 비동기 콘텍스트 내에서만 생성 및 액세스가 가능하다.

MainActor

MainActor는 애플리케이션의 메인 스레드에 연결된 액터입니다.

UIKit이나 SwiftUI 같은 UI 프레임워크와의 상호 작용을 메인 스레드(또는 UI 스레드)에서 안전하게 수행하도록 돕습니다. 이는 UI 작업이 메인 스레드에서만 수행되어야 한다는 요구사항을 충족시키기 위해 필요합니다. MainActor는 특히 UI와 관련된 작업을 메인 스레드에서 수행하도록 보장합니다.

  • @MainActor 속성을 타입, 메서드, 인스턴스, 함수, 그리고 클로저에 사용될 수 있다.
@MainActor
func updateUI() {
    // UI 업데이트 코드
}

@MainActor
class MyViewController: UIViewController {
    var data: String = "" {
        didSet {
            label.text = data
        }
    }

    @IBOutlet var label: UILabel!
    // 클래스 내 다른 메소드와 프로퍼티
}

메인 스레드에서 시간이 오래 걸리는 작업을 수행하면 애플리케이션의 반응성이 저하될 수 있으므로, 비동기 작업은 백그라운드 스레드에서 수행한 후, 그 결과만 MainActor를 통해 메인 스레드에서 처리하자.

ScenePhase

ScenePhase 는 현재 화면의 상태를 저장하기 위해 SwiftUI 에서 사용하는 @Environment 속성.

다음 예제와 같이 onChange() 수정자와 함께 쓰여, 화면 활성화/비활성화, 포그라운드/백그라운드 전환을 모니터링 할 수 있다.

import SwiftUI

@main
struct LifecycleDemoApp: App {
    
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { (oldScenePhase, newScenePhase) in
            switch newScenePhase {
            case .active:
                print("Active \(oldScenePhase)")
            case .inactive:
                print("Inactive \(oldScenePhase)")
            case .background:
                print("Background")
            default:
                print("Unknown scenephase")
            }
        }
    }
}

2024년 04월 20일

TODAY I LEARNED (오늘 배운 것)

  • Git, GitHub 끝장내기 (1/7)
  • Generics
  • Collection Types(Array, Dictionary, Set)

commit - 버전(타임캡슐) 담아 묻기

git add .
git add 파일

git commit
git commit -m ""


## add 와 커밋 메시지와 함께 커밋 (새로 추가한 파일은 포함 안되므로 git add 해야 함)
git commit -am ""

과거로 돌아가기

  • reset: 과거의 커밋으로 되돌아가는데, 과거의 커밋 이후의 커밋들은 전부 삭제
  • revert: 과거의 커밋의 변경사항만을 현재로 가져오는 것
# reset
git reset --hard {commit}

# 아직 커밋하지 않은 내용을 전부 지움. 즉 마지막 커밋 상태로 되돌아감
git reset --hard

# revert
git revert {commit}

# no commit revert
git revert --no-commit {commit}

# revert 충돌 시
git rm/add 파일
git revert --continue

branch

# branch 목록 보기
git branch

# branch 만들기 (전환은 안함)
git branch 브랜치명

# branch 전환
git switch 브랜치명

# branch 만들기와 전환
git switch -c 브랜치명

# branch 삭제하기
git branch -d 브랜치명

# branch 강제 삭제하기 (지울 브랜치에 다른 브랜치로 적용되지 않은 커밋 있을 시)
git branch -D 브랜치명

# branch 이름 변경
git branch -m (기존 브랜치명) (새 브랜치명)

# 여러 브랜치의 로그 편리하게 보기 
# (그러나 여러 브랜치 작업 내용 볼 때는 '소스 트리'로)
git log --all --decorate --onelone --graph

branch 를 합치는 두 가지 방법

  • merge

    • 두 브랜치의 최종 커밋을 하나의 새로운 커밋으로 합치는 작업!
    • 브랜치의 사용 내역들을 남겨둘 필요가 있을 경우에 사용하자.
  • rebase

    • 한 브랜치의 커밋을 다른 브랜치의 최상단에 다시 적용하는 과정.
    • 이를 통해 마치 두 브랜치가 순차적으로 개발된 것처럼 히스토리를 재정렬할 수 있음.
    • 히스토리를 깔끔하게 만드는 것이 중요할 경우, 그러나 이미 공유된 브랜치에 대해서는 협업 시 주의해서 사용해야 함.

merge로 합치기

git switch main
git merge 브랜치명

# :wq 로 자동입력된 커밋 메시지 저장하여 마무리.
# 소스트리에서 확인해주기.
# 필요 시, merge 가 끝난 브랜치는 삭제

merge 되돌리기 merge 도 하나의 커밋이므로, reset 으로 되돌리기 가능

rebase

# 1. 업데이트 하고자 하는 작업 브랜치로 switch 후, 
# 2. 작업 브랜치의 커밋들을 main 브랜치로 가지 붙이기 한 다음에(이 과정에서 충돌 발생 시 해결해야 함), 
# 3. main 브랜치가 뒤쳐져 있는 상황이기에( rebase를 통해 작업 브랜치의 변경 사항이 main의 최신 상태 위에 재배치되었기 때문에), main 브랜치시점에서 작업 브랜치와 merge 하면 됨.

git switch 작업-브랜치
git rebase main
git switch main
git merge 작업-브랜치

# 필요 시, merge 가 끝난 브랜치는 삭제

merge - 충돌 해결하기

# 당장 충돌 해결이 어려운 경우에 merge 중단
git merge --abort

# rebase도 충돌 해결 어려운 경우에 중단 가능
git rebase --abort

# 해결 가능하면, 충돌 부분 수정한 뒤, 
# git add . 
# 그리고 git rebase --continue

# rebase 충돌 해결은 CLI 로 진행 권장

원격 저장소 사용하기

# 로컬의 git 저장소에서 원격 저장소로의 연결 추가
git remote add origin (원격 저장소 주소)
  # 원격 저장소 이름: origin (이것은 바꿀 수도 있음)

# github 권장 - 기본 브랜치명을 main 으로
git branch -M main

# 로컬 저장소의 커밋 내역들 원격으로 push (업로드)
git push -u origin main

push (원격으로 커밋 업로드)

git push  # 이미 git push -u origin main 으로 대상 원격 브랜치가 지정되었기 때문에 가능

pull (동료가 한 작업을 로컬로 가져오기)

git pull

pull 할 것이 있을 때 push 를 할 경우. 로컬에서 작업하다가 원격 저장소에 push 할 수 있으려면, 로컬이 원격의 최신 내역대로 맞춰져 있어야 함. 안 그러면 충돌 발생

해결 방법.

  1. merge 방식
  • git pull —no-rebase
  • 원격 저장소의 최신 변경 사항을 현재 로컬 브랜치로 가져오는데, merge를 수행합니다.

예시 상황: 원격 저장소에는 다른 사람이 작업한 커밋이 있고, 로컬에는 내가 작업한 커밋이 있다. 실행: git pull —no-rebase 결과: 원격 저장소의 변경 사항을 로컬에 가져와 자동으로 merge합니다. 필요한 경우 수동으로 충돌을 해결합니다. 이제 로컬 브랜치는 원격 저장소의 상태와 동기화되었고, 추가적으로 내 로컬 변경 사항을 원격 저장소에 push할 수 있습니다.

  1. rebase 방식
  • git pull -rebase
  • 원격 저장소의 최신 변경 사항을 현재 로컬 브랜치로 가져오지만, merge 대신 rebase를 사용합니다. 이는 원격 저장소의 변경 사항을 기반으로 로컬에서 작업한 커밋들을 재적용하는 방식입니다. 상대방껄 먼저 가져옴.
  • 로컬 브랜치의 히스토리가 더 깔끔하게 유지되며, 마치 로컬에서의 변경 사항이 원격 저장소의 최신 변경 사항 위에 순차적으로 이루어진 것처럼 보이게 됩니다.

예시 상황: 로컬 브랜치에 내가 추가한 커밋이 있고, 원격 저장소에는 다른 커밋이 이미 push되어 있다. 실행: git pull —rebase 결과: 원격 저장소의 최신 상태를 기준으로 로컬의 커밋들이 재정렬됩니다. 충돌이 발생하면 해결 후, 작업을 계속합니다. 최종적으로 로컬 브랜치는 원격 저장소의 최신 상태에 내 변경 사항이 순차적으로 적용된 형태로 업데이트됩니다.

위 둘 중 하나의 해결 방법으로 해결 후, 내 작업 내역을 원격 저장소에 push 하자.

로컬에서 무언가를 작업하고, 이미 공유된 커밋(이미 push된 커밋)을 rebase 하여, main 브랜치위에 올리는 것을 협업 시 지양하라는 것이다. (커밋의 기존 히스토리를 변경하기 때문)

그러나, pull 할 때 사용하는 rebase 는 괜찮다. 이 때의 rebase 는 원격 저장소의 변경 사항을 로컬로 가져온 뒤, 로컬에서 진행한 작업을 원격의 최신 변경 사항 위에 재적용하는 것이다.

  1. 강제 push
  • 원격에 있는 내용들이 잘못되어서, 내 로컬의 작업 내용으로 원격에 덮어씌어야 할 경우 사용한다.
  • 원격의 내역들이 내 작업으로 덮어씌어져서 사라질 수 있기에, 미리 합의하고 사용하자.
git push --force

원격의 브랜치 다루기

로컬에서 브랜치를 새로 만들고, 원격으로 해당 브랜치를 업데이트 시키고 싶은 경우.

git push --set-upstream origin 브랜치명
git push -u origin 브랜치명
# 둘 중 아무거나 사용 가능

로컬 + 원격의 모든 브랜치 목록 확인

git branch --all
git branch -a

2024년 04월 21일

TODAY I LEARNED

SceneStorage, AppStorage

Capabilities 에 App Group 은, 워치나 위젯 또는 액티비티킷 등을 사용할 때 필요하다.

SceneStorage

https://phillip5094.tistory.com/41

AppStorage 앱 튜토리얼을 처음에만 보여주고 싶을때, 값을 토글로 관리하여 사용하기도 함.

SwiftData


2024 04 24 TIL

TODAY I LEARNED

  • segue: 뷰 간의 연결고리

  • UIKit 에서 SwiftUI View 연결하기

    • UIHostingController
  • scrollIndicatorsFlash

    • 스크롤뷰 인디케이터 가이드라인 제시할 때 사용
  • Data, View 분리

  • EditButton

  • .onMove

  • .searchable : List 에 검색 기능 추가

  • LazyStacks


2024_04_25 TIL

TODAY I LEARNED

  1. LazyGrids

  2. Scroll Programatically

    • scrollPosition (iOS 17+)
    • ScrollViewReader
  3. Expanding Lists - struct recursive

  4. DisclosureGroup - 기본적으로 닫혀있으므로(collapsed), bool 값에 바인딩하여 기본적으로 열려있게(expanded) 할 수도 있음

  5. Widget Extension

    • TimelineEntry

2024_04_26 TIL

  1. GeometryReader

    • GeometryReader VS UIScreen

      UIScreen will always give you the dimensions of the entire screen.

      A GeometryReader will give you the dimensions of whatever View contains it. That could be the entire screen, or it could be just a part of the screen. It all depends on where you put the GeometryReader in your view hierarchy.

      The two methods are used for different purposes that sometimes result in the same values but do not necessarily have to.

  2. Portrait VS LandScape

    • to compatible Both Portrait, LandScape, Use GeometryReader.

    Like this,
    if geometry.size.height > geometry.size.width {
        Image(systemName: imageName)
            .symbolRenderingMode(.multicolor)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geometry.size.width * 0.75)
    } else {
        Image(systemName: imageName)
            .symbolRenderingMode(.multicolor)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: geometry.size.height * 0.55)
    }
  3. @Environment(.colorScheme): for support DarkMode

  4. Canvas Function

    • Variants (Dynamic - Font, color - Light/Dark, Orientation - Portrait/Landscape)
    • Pin (Multi Canvas Preview)
    • ADD NavigationStack in Preview
  5. @ObservedObject - SwiftUI 의 View 에서 직접 사용할 일은 드뭄. @StateObject 를 사용하여, 뷰에 갱신 하고자 할 때, 맞음. ObservedObject 는 뷰 보다는 예를 들어, 결제 프로세스에서 구현할 만한 내용이다.


2024_04_29 TIL

  • SignInWithApple
  • Firebase
    • FirebaseRemoteConfig
    • GoogleSignIn

2024_05_05 TIL

TODAY I LEARNED: Protocol

가변 메서드 요구 값 타입(struct, enum)의 인스턴스 메서드가 인스턴스 내부의 값을 변경할 필요가 있을 때 -> mutating 을 func 키워드 앞에 적어, 메서드에서 인스턴스 내부의 값을 변경한다는 것을 명시할 수 있다.

class 의 경우 참조 타입이기 때문에, mutating 을 메서드 앞에 쓰지 않아도 인스턴스 내부 값을 바꾸는 데 문제가 없지만, 값 타입인 구조체와 열거형의 메서드 앞에는 mutating 키워드를 붙인 가변 메서드 요구가 필요하다.


Resettable 프로토콜의 가변 메서드 요구

protocol Resettable {
    mutating func reset()
}

class Person: Resettable {
    var name: String?
    var age: Int?
    
    func reset() {  // class 에는 mutating 키워드를 제외함
        self.name = nil
        self.age = nil
    }
}

struct Point: Resettable {
    var x: Int = 0
    var y: Int = 0
    
    mutating func reset() {
        self.x = 0
        self.y = 0
    }
}

enum Direction: Resettable {
    case east, west, south, north, unknown
    
    mutating func reset() {
        self = Direction.unknown
    }
}

—> 만약 Resettable 프로토콜에서 가변 메서드를 요구하지 않는다면, 값 타입의 인스턴스 내부 값을 변경하는 mutating 메서드는 구현 할 수 없다.

이니셜라이저 요구

프로토콜의 이니셜라이저 요구와 구조체의 이니셜라이저 요구 구현

protocol Named {
    var name: String { get }
    
    init(name: String)
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

클래스의 이니셜라이저 요구 구현

class Person: Named {
    var name: String
    
    required init(name: String) {    // Initializer requirement 'init(name:)' can only be satisfied by a 'required' initializer in non-final class 'Person'
        self.name = name
    }
}

클래스 타입에서 프로토콜의 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 이니셜라이저가 지정 이니셜라이저이든 편의 이니셜라이저이든 상관없이, required 식별자를 앞에 붙여 구현해야 한다.

Person 클래스를 상속받는 모든 클래스는 Named 프로토콜을 준수해야 하며, 이는 곧 상속 받는 클래스에 해당 이니셜라이저를 모두 구현해야 하기 때문이다. 그래서 만약, 클래스 자체가 상속받을 수 없는 final 클래스라면 required 식별자를 붙이지 않아도 된다. 상속할 수 없는 클래스의 required 이니셜라이저 구현은 무의미하기 때문이다.

상속 불가능한 클래스의 이니셜라이저 요구 구현

final class Person: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
} 

만약 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있는 상황에서 그 클래스를 상속받은 클래스가 있다면, required 와 override 식별자를 모두 명시하여 프로토콜에서 요구하는 이니셜라이저를 구현해주어야 한다.

상속받은 클래스의 이니셜라이저 요구 구현 및 재정의

class School {
    var name: String
    
    // School 클래스는 Named 프로토콜을 채택하지 않았지만, Named 프로토콜이 요구하는 이니셜라이저를 구현한 경우.
    init(name: String) {
        self.name = name
    }
}

class MiddleSchool: School, Named {
    // 이렇게 부모 클래스에서 프로토콜이 요구하는 이니셜라이저를 모두 구현한 경우, required 와 override 식별자를 모두 명시하도록 한다
    // School 클래스에서 상속받은 init(name: ) 이니셜라이저를 재정의해야 하며, 동시에 Named 프로토콜의 이니셜라이저 요구도 충족시켜줘야 하므로, 모두 표기한다. (override required 도 가능)
    required override init(name: String) {
        super.init(name: name)
    }
}

실패 가능한 이니셜라이저 요구를 포함하는 Named 프로토콜과 Named 프로토콜을 준수하는 다양한 타입들


protocol Named {
    var name: String { get }
    
    init?(name: String)
}

struct Animal: Named {
    var name: String
    
    init!(name: String) {
        self.name = name
    }
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Person: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}

class School: Named {
    var name: String
    
    required init?(name: String) {
        self.name = name
    }
}

실패 가능한 이니셜라이저를 요구하는 프로토콜을 준수하는 타입은 해당 이니셜라이저를 실패 가능한 이니셜라이저로 구현하거나 일반적인 이니셜라이저로 구현 하는 방법 모두 유효하다.


프로토콜의 상속과 클래스 전용 프로토콜

기존 프로토콜에 더 많은 요구사항을 추가하기 위해, 하나 이상의 프로토콜을 상속 받을 수 있다. 프로토콜 상속 문법은 기존 클래스의 상속 문법과 유사하다.

프로토콜의 상속

protocol Readable {
    func read()
}

protocol Writeable {
    func write()
}

protocol ReadSpeakable: Readable {
    func speak()
}

protocol ReadWriteSpeakable: Readable, Writeable {
    func speak()
}

class SomeClass: ReadWriteSpeakable {
    func read() {
        print("Read")
    }
    
    func write() {
        print("Write")
    }
    
    func speak() {
        print("Speak")
    }
}

프로토콜의 상속 리스트에 class 키워드를 추가하여 프로토콜이 클래스 타입에만 채택될 수 있도록 제한할 수 있다. 클래스 전용 프로토콜 제한을 두기 위해서, 프로토콜 상속 리스트 맨 앞에, class 키워드 추가.

class 전용 프로토콜의 정의

protocol ClassOnlyProtocol: class, Readable, Writeable {
    
}

class SomeClass: ClassOnlyProtocol {
    func read() { }
    func write() { }
}

// Non-class type 'SomeStruct' cannot conform to class protocol 'ClassOnlyProtocol'
struct SomeStruct: ClassOnlyProtocol {
    func read() { }
    func write() { }
}

프로토콜 조합

하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 할 때, 여러 프로토콜을 한 번에 조합하여 요구할 수 있다.

AProtocol & BProtocol 과 같이 표현한다.

프로토콜 조합 및 프로토콜, 클래스 조합

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

class Car: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Truck: Car, Aged {
    var age: Int
    
    init(name: String, age: Int) {
        self.age = age
        super.init(name: name)
    }
}

func celebrateBirthday(to celebrator: Named & Aged)  {
    print("Happy birthday \(celebrator.name)!! Now you are \(celebrator.age)")
}

let iyungui = Person(name: "iyungui", age: 25)

celebrateBirthday(to: iyungui)

let myCar: Car = Car(name: "Genesis Motor")
//celebrateBirthday(to: myCar)    // Argument type 'Car' does not conform to expected type 'Aged'

// class & protocol 조합에서 class 타입은 한 타입만 조합 가능
//var someVariable: Car & Truck & Aged // Protocol-constrained type cannot contain class 'Truck' because it already contains class 'Car'

// Car class 의 인스턴스 역할을 수행할 수 있고, Aged 프로토콜을 준수하는 인스턴스만 할당할 수 있다. !!
var someVariable: Car & Aged

// Truck 인스턴스는 Car 클래스 역할도 할 수 있고, Aged 프로토콜도 준수하므로 할당할 수 있다.
someVariable = Truck(name: "Truck", age: 5)

프로토콜 준수 확인

  1. is 연산자를 통해 해당 인스턴스가 특정 프로토콜을 준수하는 지 확인
  2. as? 다운캐스팅 연산자를 통해 다른 프로토콜로 다운 캐스팅 시도
  3. as! 다운캐스팅 연산자를 통해 다른 프로토콜로 강제 다운 캐스팅

프로토콜 확인 및 캐스팅

print(iyungui is Named)
print(iyungui is Aged)

print(myCar is Named)
print(myCar is Aged)


if let castedInstance: Named = iyungui as? Named {
    print("\(castedInstance) is Named")
} // Person is Named

if let castedInstance: Named = myCar as? Named {
    print("\(castedInstance) is Named")
    // Car is Named
}

if let castedInstance: Aged = myCar as? Aged {
    print("\(castedInstance) is Aged")
}

프로토콜의 선택적 요구 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있다. 이 때 해당 프로토콜에 @objc 속성을 부여해야 한다. (Foundation 라이브러리 임포트 필요)

objc 속성이 부여된 프로토콜은 Objective-C 클래스를 상속받은 클래스에서만 채택할 수 있다. (열거형, 구조체는 채택할 수 없다.)

@objc protocol Moveable {
    func walk()
    @objc optional func fly() // optional 식별자 필요
}

// objc 속성의 프로토콜을 사용하기 위해 Objective-C 클래스인 NSObject를 상속 받음.
class Tiger: NSObject, Moveable {
    func walk() {
        print("Tiger walks")
    }
}

class Bird: NSObject, Moveable {
    func walk() {
        print("Bird walks")
    }
    
    func fly() {
        print("Bird flys")
    }
}

let tiger: Tiger = Tiger()
let bird: Bird = Bird()

tiger.walk()
bird.walk()
bird.fly()
var moveableInstance: Moveable = tiger
moveableInstance.fly?() // nil

moveableInstance = bird
moveableInstance.fly?() // Bird flys

프로토콜 변수와 상수

프로토콜은 프로토콜 이름만으로 자기 스스로 인스턴스를 생성하고 초기화할 수는 없다. 그러나, 프로토콜 변수나 상수를 생성하여 특정 프로토콜을 준수하는 타입의 인스턴스를 할당할 수 있다.

protocol Named {
    var name: String { get }

    init?(name: String)
}

struct Animal: Named {
    var name: String
    
    init!(name: String) {
        self.name = name
    }
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Person: Named {
    var name: String

    required init(name: String) {
        self.name = name
    }
}

class School: Named {
    var name: String
    
    required init?(name: String) {
        self.name = name
    }
}

var someNamed: Named = Animal(name: "Animal")
someNamed = Pet(name: "Pet")
someNamed = Person(name: "Person")
someNamed = School(name: "School")

Pet, Person, School 타입은 모두 Named 프로토콜을 준수한다. 그래서, Named 프로토콜을 타입으로 갖는 변수 someNamed 에는 Pet, Person, School 타입의 인스턴스를 할당될 수 있다.

위임을 위한 프로토콜


2024_05_09 TIL

  • Protocol

  • Extension

    • Extensions add new functionality to an existing class, structure, enumeration, or protocol type.
    • you don’t have access to the original source code
  • Error Handling

  • Covariance / Contravariance . (some, any)


2024_05_10 TIL

  • UIKit - TableView

    • UITableViewDataSource, UITableViewDelegate
  • mutating

  • UITextFieldDelegate - resignFirstResponder (hide keyboard)


2024_05_19 TIL

메모리 관리와 참조

Swift 에서는 메모리 관리를 위해 ARC(Automatic Reference Counting) 방식을 사용한다. ARC 는 더 이상 객체가 필요하지 않을 때 자동으로 메모리를 해제해주는 역할을 한다. 이 과정에서 객체에 대한 참조 횟수를 추적한다.

참조 횟수란?

  • 객체를 가리키는 변수, 상수, 프로퍼티의 수
  • 이 횟수가 0이 되면 ARC는 객체의 메모리를 자동으로 해제함

참조의 종류

Swift 에서 참조는 크게 강한 참조(Strong Reference), 약한 참조(Weak Reference), 비소유 참조(unowned Reference) 세 가지로 나뉜다.

  1. 강한 참조(strong reference)
  • 기본적으로 변수나 상수에 객체를 할당하면, 강한 참조가 생성된다.
  • 강한 참조는 참조 횟수를 증가시키고, 이는 해당 객체가 메모리에서 유지되도록 보장한다.
var objectA: MyClass? = MyClass()
var objectB = objectA   // objectA 와 objectB 는 같은 객체를 강한 참조
  1. 약한 참조(weak reference)
  • 약한 참조는 객체의 참조 횟수를 증가시키지 않는다.
  • 약한 참조로 연결된 객체가 메모리에서 해제되면, 참조 객체가 자동으로 nil이 된다.
  • 약한 참조는 주로 순환 참조를 방지하는 데 사용된다.
class MyClass {
    weak var delegate: SomeDelegate?
}
  1. 비소유 참조(unowned reference)
  • 비소유 참조도 객체의 참조 횟수를 증가시키지 않지만, 약한 참조처럼 참조가 nil로 자동으로 바뀌지는 않는다.

  • 비소유 참조는 참조하는 객체가 자신과 같거나 더 긴 생명주기를 가질 때 사용된다.

  • self가 클로저의 생명 주기 동안 반드시 존재한다고 가정할 때 사용

  • 비소유 참조 사용 시 주의점은, 참조하는 객체가 해제된 후에 접근하려 하면 런타임 에러

=> 이해하기 쉽게 말하자면, [unowned self]는 “이 클로저가 실행되는 동안에는 self가 항상 살아있을 거야”라고 믿고 사용하는 것

class Customer {
    let name: String
    unowned let card: Card
    init(name: String, card: Card) {
        self.name = name
        self.card = card
    }
}

순환 참조(Cyclic reference) 클래스 인스턴스 사이에 강한 참조가 서로를 가리키게 되면, 참조 횟수가 절대 0이 되지 않아 메모리에서 해제되지 않는 순환 참조가 발생하게 된다. -> 이는 메모리 누수로 이어짐.

class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
}

class Apartment {
    let unit: String
    var tenant: Person?
    init(unit: String) { self.unit = unit }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unut: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil
unit4A = nil

여기서 john 과 unit4A 는 서로를 강하게 참조하고 있기 때문에, 둘 다 nil로 설정해도 메모리에서 해제되지 않는다.

순환 참조 방지 순환 참조를 방지하기 위한 주요 방법은 약한 참조와 비소유 참조를 적절히 사용하는 것이다.

  1. 약한 참조 사용하기: 일반적으로 부모 - 자식 관계에서 자식이 부모를 약하게 참조하는 방법을 사용한다.
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
}

class Apartment {
    let unit: String
    weak var tenant: Person?    // weak reference
    init(unit: String) { self.unit = unit }
}

var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

john = nil  // 이제 Person 인스턴스는 메모리에서 해제.
unit4A = nil  // 이제 Apartment 인스턴스도 메모리에서 해제.
  1. 비소유 참조 사용하기

서로 참조는 하지만, 학 객체가 다른 객체보다 항상 먼저 메모리에서 해제되는 경우

class Customer {
    let name: String
    var card: Card
    init(name: String) { self.name = name }
}

class Card {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

var john: Customer? = Customer(name: "John")
john!.card = Card(number: 1234_5678_9012_3456, customer: john!)
john = nil  // Card 인스턴스도 자동으로 해제됨.

클로저에서의 순환 참조 클로저 내부에서 self를 캡쳐할 때도 순환 참조가 발생할 수 있다. 이를 방지하기 위해 클로저 캡처 목록에서 weak나 unowned를 사용한다.

클로저에서 약한 참조 사용 예시

class MyClass {
    var name = "MyClass"
    var closure: (() -> Void)?

    func setupClosure() {
        closure = { [weak self] in  // self 를 약하게 캡쳐
            guard let self = self else { return }
            print(self.name)
        }
    }
}

var instance: MyClass? = MyClass()
instance?.setupClosure()
instance?.closure?()    // "MyClass 출력"

instance = nil  // MyClass 인스턴스 메모리 해제

2024_05_20 TIL

01-UIView와 Auto Layout 사용하기

02-UIKit을 사용한 사용자 인터페이스 디자인

  • UIStackView

    • UIStackView 는 세로(수직) 또는 가로(수평) 방향으로 뷰를 쌓음 - axis
    • arrangedSubviews 를 사용하여 스택에 뷰를 추가
    • alignment 와 distribution 속성을 통해 스택 내부 뷰의 정렬과 크기 분배 방식을 설정
    • 오토레이아웃을 사용하여 UIStackView 의 위치와 크기를 설정
  • addTarget -> addAction

  • spacer

let spacer = UIView()
spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
spacer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

2024_05_21 TIL

Xcode ShortCuts

in simulator

  • toggle light/dark mode

⌘ ⇧ A

  • Rotate simulator

⌘ ← / ⌘ →

  • Home screen

⌘ ⇧ H

  • Screen Shot

⌘ S

  • Toggle keyboard visibility

⌘ ⇧ K

in Xcode

  • stop running app

    com + .

Git Command

  • rename commit message
git commit -amend
  • cancel latest commit (restore history in working directory)
git reset --soft HEAD^

git commit -m "new commit message"

2024_06_17 TIL

Combine 에 대한 설명