Opaque Types과 boxed protocol type타입 유출을 방지하면서 얻는 장점
Generic과 Opaque Types 모두 공통적으로 타입에 대한 유연성을 확보할 수 있다. 두 키워드 모두 컴파일 시점에 타입이 결정되지만, Generic의 경우 타입이 선언자 외부로 유출되는 반면 Opaque Types의 경우 타입이 선언자 외부로 유출되지 않는다.
즉, 이를 통해 좀 더 높은 추상화 수준을 확보할 수 있게 된다. 따라서 내부 구현의 변경이 발생해도 특정 프로토콜만 준수할 경우 그 어떠한 인터페이스에 대해서도 대응이 가능해진다.
Opaque Types과 boxed protocol type 의 차이
Opaque Types
boxed protocol type
스유에서 기본 뷰 구조
struct AboutModifier: View {
var body: some View {
Text("Hello")
}
}
View 프로토콜 채택some View 채택func modifier<T>(_ modifier: T) -> ModifiedContent<Self, T>
associatedtype
Generic의 개념을 프로토콜에 적용시킨 것.사용법
View, Equatable 등)//SwiftUI의 View 프로토콜 내부
public protocol View {
//연관 타입 Body 선언
associatedtype Body : View
//연관 타입 Body 타입인 body 계산 프로퍼티
@ViewBuilder @MainActor var body: Self.Body { get }
}
struct ContentView: View {
var body: some View {
}
}
빌드 시 에러 발생
발생 원인
View 프로토콜을 채택한 ContentView는 필수적으로 body 프로퍼티를 선언해야 한다.body는 some View 타입이며, 이러한 타입을 리턴해야 한다.body 프로퍼티 내부가 비어 있거나, 반환 타입을 명확히 추론할 수 없는 경우, Swift 컴파일러는 "Property declares an opaque return type, but has no initializer expression from which to infer an underlying type"와 같은 컴파일 에러를 발생시킨다.body가 구체적인 뷰 타입을 반환하도록 요구, 따라서 body의 내부에는 특정 View를 반드시 초기화해야 한다.개념
wrappedValue
Property Wrapper 내부에서 실제로 저장되는 값을 말함Property Wrapper 내부의 연산 로직 등을 처리wrappedValue
Property Wrapper 의 추가 정보나 기능을 제공하기 위해 사용$ 키워드를 통해 접근코드 예시
@propertyWrapper
struct DecimalValue {
var defaultValue: String
var projectedValue = "아무거나"
var wrappedValue: String {
get {
return defaultValue
}
*set {*
let result = Int(newValue)!.formatted(.number)
defaultValue = result
projectedValue = "이체할 금액은 \(result)입니다."
}
}
}
struct Example {
@DecimalValue(defaultValue: "0원")
var number
}
var example = Example()
example.number = "100000"
example.number // 1000,000원
example.$number // 이체할 금액은 1000,000원입니다.
Extract Subview 옵션 선택struct ContentView: View {
var body: some View {
//View 1
VStack(spacing: 10) {
Image(systemName: "star.fill")
.background(.red)
Text("토스증권")
.background(.green)
}
.padding()
.background(.yellow)
//View 2
VStack(spacing: 10) {
Image(systemName: "star.fill")
.background(.red)
Text("토스증권")
.background(.green)
}
.padding()
.background(.yellow)
//View 3
VStack(spacing: 10) {
Image(systemName: "star.fill")
.background(.red)
Text("토스증권")
.background(.green)
}
.padding()
.background(.yellow)
}
}
//Extract Subview
struct ContentView: View {
var body: some View {
HStack {
CardView(name: "토스 증권", imageName: "star")
CardView(name: "토스 증권", imageName: "star")
CardView(name: "토스 증권", imageName: "star")
}
}
}
struct CardView: View {
var name: String
var imageName: String
var body: some View {
VStack(spacing: 10) {
Image(systemName: imageName)
.background(.red)
Text(name)
.background(.green)
}
.padding()
.background(.yellow)
}
}
ViewModifier 프로토콜을 채택한 구조체를 생성func body(content: Content) -> some View 구현content에 원하는 Modifier를 추가예시코드
//파일 내부에서 private으로 설정 요망
private struct PointBorderText: ViewModifier {
func body(content: Content) -> some View {
content
.font(.title)
.padding(10)
.foregroundColor(.white)
.background(.purple)
.clipShape(.capsule)
}
}
extension View {
func pointBorderText() -> some View {
modifier(PointBorderText())
}
}
struct CardView: View {
var body: some View {
VStack(spacing: 10) {
Image(systemName: imageName)
.background(.red)
Text(name)
.pointBorderText()
}
}
}

데이터 관리
@State: 프로퍼티(데이터)에 대한 상태를 저장하고 관찰, private을 통해 해당 View에서만 유효하다는 것을 명시@Binding: 상위 뷰가 가진 상태를 하위 뷰에서 사용하고 관찰한다. $라는 접두어를 통해 접근코드 예시
@State 를 통해 데이터 바인딩struct TamagochiView: View {
@State private var riceCnt: Int = 0
@State private var waterCnt: Int = 0
var body: some View {
VStack {
Text("밥알 갯수\(riceCnt)")
Button("증가") {
riceCnt += 1
}
}
VStack {
Text("물 갯수\(waterCnt)")
Button("증가") {
waterCnt += 1
}
}
}
}
@State 프로퍼티 래퍼로 감싸준다.@Binding을 통해 하위뷰에 데이터 전달. struct TamagochiView: View {
@State private var riceCnt: Int = 0
@State private var waterCnt: Int = 0
var body: some View {
VStack {
CountView(cnt: $riceCnt, name: "밥알")
CountView(cnt: $waterCnt, name: "물")
}
}
}
struct CountView: View {
@Binding var cnt: Int
let name: String
var body: some View {
VStack {
Text("\(name) 갯수\(cnt)")
Button("증가") {
cnt += 1
}
}
}
}
차이점
@State는 뷰 내부에서 정의되고 관리되는 반면, @Binding은 외부에서 정의된 데이터에 대한 참조이다.@State는 해당 뷰의 로컬 상태를 나타내고, @Binding은 다른 상태나 속성에 바인딩되어 있다.@State는 주로 뷰의 내부 상태를 관리하는데 사용되고, @Binding은 부모-자식 뷰 간의 데이터 공유 및 동기화에 사용된다.
Push
NavigationView를 활용해 구성View를 NavigationView로 감싸준 다음, NavigationLink를 통해 화면 전환struct PushTransitionView: View {
var body: some View {
NavigationView {
NavigationLink("Push") {
ContentView() //이동할 View
}
}
}
}
Sheet, Full
.sheet, .fullScreenCover modifier를 통해 화면 전환struct PullSheetTransitionView: View {
@State private var isFull = false
@State private var isSheet = false
var body: some View {
HStack {
transitionButton("Full", trigger: $isFull)
.fullScreenCover(isPresented: $isFull, content: {
ContentView()
})
transitionButton("Sheet", trigger: $isSheet)
.sheet(isPresented: $isSheet, content: {
ContentView()
})
}
}
}
func transitionButton(_ title: String, trigger: Binding<Bool>) -> some View {
Button(title) {
trigger.wrappedValue.toggle()
}
}
isPresented 파라미터의 타입은 Binding<Bool>이다.@Binding을 통해 상태 값을 동기화 시켜준다. 즉, 뒤로가기 버튼을 클릭하면 isFull의 값을 False로 전환var body: some View {
ScrollView {
AsyncImage(url: url) { data in
switch data {
case .empty:
ProgressView()
case .success(let image):
image
case .failure(let error):
Image(systemName: "star")
@unknown default:
Image(systemName: "star")
}
}
}
}
case .success(let image)에서 불러온 이미지를 설정할 수 있다.NavigationLink는 초기화 시 내부의 뷰를 즉시 생성NavigationLazyView를 활용해 사용자가 링크에 접근 시 생성하도록 설정struct NavigationLazyView<T: View>: View {
let build: () -> T
init(_ build: @autoclosure @escaping () -> T) {
self.build = build
}
var body: some View {
build()
}
}
@autoclosure 중괄호 생략 키워드//Full된 뷰
@Environment(\.presentationMode) **var** presentationMode
.
.
.
presentationMode.wrappedValue.dismiss() //dismiss()
@Environment는 앱 전체에 사용되는 환경 변수의 역할.presentationMode, .colorScheme등이 있고 커스텀도 가능함.//다크, 라이트 모드 대응
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme // <-
var body: some View {
Text("Hello, world!")
.padding()
.foregroundColor(colorScheme == .dark ? .black : .white)
}
}//NavigationView
var body: some View {
NavigationView { // This is deprecated.
List {
NavigationLink("Purple") {
ColorDetail(color: .purple)
}
NavigationLink("Pink") {
ColorDetail(color: .pink)
}
NavigationLink("Orange") {
ColorDetail(color: .orange)
}
}
}
.navigationViewStyle(.stack)
}
//NavigationStack
var body: some View {
NavigationStack {
List {
NavigationLink("Mint", value: Color.mint)
NavigationLink("Pink", value: Color.pink)
NavigationLink("Teal", value: Color.teal)
}
.navigationDestination(for: Color.self) { color in ✅
ColorDetail(color: color)
}
.navigationTitle("Colors")
}
}