TIL(Today I Learned)
1월 30일(일)
학습내용
- SwiftUI(CS193p Lecture 6)
- protocol
-
함수와 변수를 갖고 있지만 실제 구현은 없는 것.
protocol Moveable { func move(by: Int) var hasMoved: Bool { get } var distanceFromStart: Int { get set } }
-
프로토콜을 여러개 채택할 수 있다.
class Car: Vehicle, Impoundable, Leasable {}
-
프로토콜은 어떻게 사용되는가?
-
드물게 타입으로서 사용된다.
func travelAround(using moveable: Moveable) // Car, PortableThing 등을 넣을 수 있다. let foo = [Moveable] // Car, PortableThing 등을 저장할 수 있는 배열
-
일반적으로 struct, class, enum 이 준수해야할 행위를 규정?하기 위해 사용된다.
struct EmojiMemoryGameView: View class emojiMemoryGame: ObservableObject
-
“don’t cares” 를 “somewhat cares”로 만들기 위해 사용된다.
struct MemoryGame<CardContent> where CardContent: Equtable
- 제네릭 타입을 특정 프로토콜을 준수하는지 여부로 제한할 수 있다는 의미이다.
- protocol-oriented programming을 위한 핵심
-
extension을 특정 프로토콜로 한정하기 위해 프로토콜을 사용한다.
extension Array where Element: Hashable { ... }
- Hashable을 준수하는 배열만 기능 확장하게 된다.
-
함수의 작동을 제한하기 위해 프로토콜을 사용된다.
init(data: Data) where Data: Collection, Data.Element: Identifiable
- Collection이면서 원소는 Identifiable한 인자만 받을 수 있도록 제한하는 것이다.
- 두 엔티티 간의 상호 조약?을 위해 프로토콜을 사용한다.
- DropDelegate
- 시스템은 DropDelegate를 채택한 타입만 Drag & Drop을 가능하게 한다. (타입은 DropDelegate에서 요구하는 함수와 변수를 반드시 구현해야한다.)
- DropDelegate
- Code Sharing의 이점을 위해 프로토콜을 사용한다.
- 프로토콜의 extension을 통해 구현을 추가할 수 있다.
- 예) 뷰들이 foregroundColor, font 등의 modifier를 갖는다.
- 프로토콜의 함수나 변수의 기본 구현을 extension을 통해 가능하다.
- 이는 POP(프로토콜 지향 프로그래밍)의 키포인트이다.
- func filter(_ isIncluded: (element) → Bool) → Array
- 이 함수는 애플에 의해 구현되었으며, Array, Range, String, Dictionary 등에서 사용된다.
- 이는 Sequence Protocol 을 extension 하여 filter를 추가하였기 때문이다.
- Array, Range, String, Dictionary 모두 Sequence 프로토콜을 채택하였다. 그래서 이들 모두 filter 함수가 저절로 추가가 된 것이다.
- Adding Protocol implementation
- 프로토콜을 Constrains and gains 이라고 생각할 수도 있다. (교수님 왈)
-
아래처럼 테슬라가 Vehicle을 채택함으로써 프로토콜에서 요구하는 함수와 변수를 구현해줘야하는 제약이 따르지만, 프로토콜의 확장기능을 그대로 얻을 수 있다는 이점이 있다.
struct Tesla: Vehicle { // Tesla is constrained to have to implement everything in vehicle // but it gains all the capabilities a Vehicle has too } extension Vehicle { func registerWithDMV() {} }
-
- 프로토콜을 Constrains and gains 이라고 생각할 수도 있다. (교수님 왈)
- View
-
View 프로토콜을 채택하면 body를 구현해줘야 하지만 아래의 기본 구현들을 얻을 수 있다.
protocol View { var body: some View } extension View { func foregroundColor(_ color: Color) -> some View { /* 구현 */ } func font(_ font: Font?) -> some View { /* 구현 */ } func blur(radius: CGFloat, opaque: Bool) -> some View { /* 구현 */ } ... }
-
- 왜 프로토콜을 사용하는가?
- 프르토콜을 통해 프로토콜을 채택한 타입이 할 수 있는 것이 무엇인지를 알 수 있다.
- 프로토콜이 요구하는 기능과 변수를 구현해야 하기 때문이라고 생각한다.
- 프로토콜을 통해 특정 행위가 가능한 타입을 요구 할 수 있다.
- 입력 파라미터가 특정 프로토콜을 준수하는 타입의 데이터만 넘길 수 있도록 할 수 있을 것이라 생각한다.
- 프로토콜을 통해 기능에 초점을 맞추고, 세부 구현에 대해서는 숨길 수 있다.
- 프로토콜을 준수한 타입의 기능이 어떻게 구현되어있는지에 대해서 알 필요 없이, 준수헀다는 것만으로 타입을 사용할 수 있기 때문이라고 생각한다.
- 프르토콜을 통해 프로토콜을 채택한 타입이 할 수 있는 것이 무엇인지를 알 수 있다.
- Generics + Protocols
- 프로토콜도 제네릭을 이용하여 내부의 프로퍼티를 일반화 시킬 수 있다.
-
다른 타입들은 <>을 이용하는 것과는 달리, associatedtype이라는 키워드를 이용한다.
protocol Identifiable { associatedtype ID var id: ID { get } }
-
ID 를 Hashable로 제한하기 위해서는 아래와 같이 할 수 있다.
protocol Identifiable { associatedtype ID where ID: Hashable // 또는 associatedtype ID: Hashable var id: ID { get } }
-
Hashable
protocol Hashable { func hash(into hasher: inout Hasher) // Hasher는 combine 함수를 가지고 있다. } struct Foo: Hashable { var i: Int var s: String func hash(into hasher: inout Hasher) { hasher.combine(i) hasher.combine(s) } }
- Protocol Inheritance
-
Hashable은 == 연산자를 이용해 두개 값의 해쉬 값이 같은지 비교할 수 있어야 하기 때문에 Equatable을 상속한다.
protocol Hashable: Equatable { func hash(into hasher: inout Hasher) }
-
Protocol Inheritance은 클래스의 상속과 다르다.
-
- Equatable
-
Equatable의 lhs와 rhs는 Self 타입인데, Self는 Equatable을 채택한 타입 자기 자신을 의미한다. 이를 Self-referencing protocol 이라고 하는데, 이는 일반 타입으로는 사용할 수 없다. 예를 들어, var x: [Equatable]로 사용할 수 없다.
protocol Equatable { static func ==(lhs: Self, rhs: Self) -> Bool }
- 따라서 서로 다른 타입의 값을 == 연산은 불가능하다. (자기자신과 같은 타입만 인자로 받을 수 있으므로)
-
View도 Self-referencing protocol이어서, var myView: View 이런식으로 사용할 수 없다.
-
- @escaping
- ViewBuilder 클로저를 파라미터로 받을때 escaping으로 받지 않으면 “Assigning non-escaping parameter ‘content’ to an @escaping closuere”라는 오류가 나타난다.
- init의 외부(프로퍼티 및 메서드)로 클로저가 전달되기 때문이다.
- 또한 escaping이 아니면 컴파일러는 인라인으로 해당 클로저를 삽입한다. (클로저를 객체로 만들어서 참조하는 것이 아니라)
struct AspectVGrid<Item, ItemView>: View where ItemView: View, Item: Idetifiable { var items: [Item] var aspectRatio: CGFloat var content: (Item) -> ItemView init(items: [Item], aspectRatio: CGFloat, @ViewBuilder content: @escaping (Item) -> ItemView) { self.items = items self.aspectRatio = aspectRatio self.content = content } }
-
-
- Shape [51:37]
- Shape 뷰를 상속하는 프로토콜이다. 따라서 모든 shape는 뷰이다. RoundedRectangle , Circle 등이 있다.
-
Shape는 foreground color 을 채움으로써 그려진다. .stroke()나 .fill()을 이용하여 선이나 면에 대한 스타일 변경이 가능하다.
func fill<S>(_ whatToFillWith: S) → some View where S: ShapeStyle
- fill의 파라미터 타입은 Generic 타입이며, ShapeStyle을 채택한 것을 입력으로 받을 수 있다.
- 예) Color, ImagePaint, AngularGradient, LinearGradient 등
- 나만의 Shape을 만들려면?
- Shape 프로토콜을 채택해야한다.
- Shape 프로토콜의 extension에 body var이 기본적으로 구현되어 있다. (따로 구현할 필요 없다.)
-
하지만 path 메서드를 구현해야한다.
func path(in rect: CGRect) → Path { return a Path }
- Path은 lines, arcs, bezirer curves 등을 통해 구성된다.
- Shape는 뷰와 다르게, animate이 가능하다.
-
따라서 내부 프로퍼티에서 animate 동안 변하는 프로퍼티는 var로 선언해준다.
struct Pie: Shape { var startAngle: Angle var endAngle: Angle ... }
-
- Shape의 Path를 그릴때 사용되는 좌표 시스템의 원점은 좌상단이며, 오른쪽으로 x가 증가하고 아래로 갈 수록 y 값이 증가하는 좌표평면이다. (일반적으론 좌하단이 원점이고, 오른쪾으로 갈수록 x가 증가하고, 위로 올라갈수록 y가 증가하지만 Shape는 반대이다.) [1:19:34]
- protocol
참고