Skip to content

SwiftUI 에 대한 내용을 끄적인 문서입니다.

Notifications You must be signed in to change notification settings

Swift-at-Night/SwiftUI-Note

Repository files navigation

SwiftUI-Note

SwiftUI

SwiftUI 에 대한 내용을 끄적인 문서입니다.

Color

그라데이션이 적용된 뷰

LinearGradient(gradient:startPoint:endPoint) 를 사용해 그라데이션 뷰를 생성할 수 있습니다.

LinearGradient(gradient: Gradient(colors: [.green, .purple]), 
               startPoint: .leading, 
               endPoint: .trailing)

Toggle

토글 스위치 색 변경하기

SwitchToggleStyle(tint:)toggleStyle(_:) modifier에 적용하여 원하는 색으로 변경할 수 있다.

Toggle("Blue", isOn: $isOn)
  .toggleStyle(SwitchToggleStyle(tint: .blue))

Button

버튼 스타일 커스터마이징 하기

ButtonStyle 을 준수하는 struct 를 생성하고 makeBody(configuration:) 델리게이트 메소드를 구현하여 버튼 스타일을 커스터마이징 할 수 있습니다.

// 배경에 그라데이션이 적용된 스타일
struct LinearGradientStyle: ButtonStyle {
 
    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .foregroundColor(.white)
            .frame(minWidth: 0, maxWidth: .infinity)
            .padding()
            .background(
                LinearGradient(gradient: Gradient(colors: [.green, .purple]), startPoint: .leading, endPoint: .trailing)
            )
            .cornerRadius(40)
            .padding(.horizontal, 20)
    }
}
// 사용 예시
Button(action: ...) {
    ...
}
.buttonStyle(LinearGradientStyle())

둥근 테두리

RoundedRectangle 뷰에 stroke 를 적용하면 둥근 테두리를 구현할 수 있습니다.

RoundedRectangle(cornerRadius: 28)
    .stroke(Color.red, lineWidth: 2)

Image

NavigationLink 또는 Button에서 사용되는 이미지가 파란색일 때

NavigationLinkButton 내부에서 쓰는 이미지는 파란색으로 덮어씌워지게 됩니다. 이를 해결하는 방법은 두가지가 있습니다.

PlainButtonStyle()

NavigationLink(destination: ...) {
    Image(...)
}
.buttonStyle(PlainButtonStyle())


Button(action: ...) {
    Image(...)
}
.buttonStyle(PlainButtonStyle())

renderingMode()

NavigationLink(destination: ...) {
    Image(...)
        .renderingMode(.original)
}

List

List 에서 id 파라미터 생략하기

data 로 사용하는 structIdentifiable 를 준수하면 List에서 id를 명시하지 않아도 됩니다.

struct Food: Identifiable {
    let id = UUID()
    ...
}

Before

List(foods, id: \.name) { ... }

After

List(foods) { ... }

List 구분선(Separator) 제거하기

iOS 13 iOS 14

iOS14

ScrollView 안에 LazyVStack 을 사용하여 모든 구분선를 제거할 수 있습니다.

또한 기본적으로 리스트 아래에 추가적인 구분선들이 존재하지 않습니다(iOS13은 data와 무관하게 구분선들이 기본으로 그려졌음)

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
}

iOS13

UITableView.appearance().separatorStyle = .none 을 사용해 구분선을 제거할 수 있습니다.

// 1. onAppear에서 호출하기
List {
   ...
}
.onAppear {
   UITableView.appearance().separatorStyle = .none
}

// 2. init에서 호출하기
init() {
    UITableView.appearance().separatorStyle = .none
}

ScrollView

Horizontal ScrollView 만들기

  1. dataForEach 로 감싸서 각 data들에 대한 뷰를 그립니다
  2. ForEachHStack으로 감싸서 가로로 배열되게 합니다
  3. HStackScrollView(.horizontal)로 감싸면 됩니다.
// 1. onAppear에서 호출하기
ScrollView(.horizontal) {
   HStack {
        ForEach(foods, id: \.name) { foods in
            Text(food.name)
        }
    }
}

ScrollView 의 스크롤바 제거하기

ScrollViewshowsIndicators 값을 false 로 세팅합니다.

// 1. onAppear에서 호출하기
ScrollView(showsIndicators: false) {
   ...
}

뷰 전환

전체화면 형태의 모달 뷰(밑에서 올라오는 뷰) 만들기

iOS 14

ViewfullScreenCover() 를 사용하여 전체화면의 뷰를 밑에서 올라오도록 할 수 있습니다.

struct BelowView: View {
    @State private var isPresented = false

    var body: some View {
        Button("뷰 띄우기") {
            self.isPresented.toggle()
        }
        .fullScreenCover(isPresented: $isPresented, content: UpperView.init)
    }
}
struct FullScreenModalView: View {
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Button("돌아가기") {
            presentationMode.wrappedValue.dismiss()
        }
    }
}

iOS 13

ZStack, StatewithAnimation, 그리고 offset 을 사용하여 밑에서 올라오는 뷰를 만들 수 있습니다.

struct BelowView: View {
    @State var showingUpper: Bool = false   // 1. 뷰를 올릴지 내릴지 결정할 State 값 
    
    var body: some View {
        ZStack {
            Text("Show upper view")
                .onTapGesture {             // 2. "보여주기"를 탭했을 때 State 값을 true로 변경
                    withAnimation {
                        showingUpper = true
                    }
                }
            
            UpperView(showingUpper: $showingUpper)  // 3. 위로 올라올 뷰는 State 값을 바인딩
                // 4. 뷰 오프셋의 y값을 State 값에 따라 바뀌도로 함 (true이면 y = 0, false 이면 y = 스크린 높이)
                .offset(x: 0, y: showingUpper ? 0 : UIScreen.main.bounds.height)
        }
    }
}
struct UpperView: View {
    @Binding var showingUpper: Bool         // 5. (3)번에서 바인딩한 프로퍼티
    
    var body: some View {
        Text("Hide upper view")
            .onTapGesture {                 // 6. 바인딩 값을 false로 변경하여  (4)번의 오프셋 값으 변경
                withAnimation {
                    showingUpper = false
                }
            }
    }
}

Text

날짜 보여주기

Text를 생성할 때 String 값이 아닌 Date 타입의 값을 넣어 날짜를 보여줄 수 있습니다. style 를 적용할 수도 있습니다.

  Text(Date().addingTimeInterval(600), style: .date)

한 개의 builder 안에 최대 몇 개까지 들어갈까?

VStack 기준, 10개를 넘기면 에러가 발생합니다: Extra argument in call

App

AppDelegate 메소드 사용하기

iOS 14

UIApplicationDelegate 를 준수하는 클래스를 App@UIApplicationDelegateAdaptor(클래스타입) 속성에 해당 클래스 타입을 명시하여 프로퍼티를 선언해주면 됩니다.

@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
class AppDelegate: NSObject, UIApplicationDelegate {
    func application(..., didFinishLaunchingWithOptions...) {
        ...
    }
}

some View

이슈해결방법 Function declares an opaque return type, but the return statements in its body do not have matching underlying types

https://developer.apple.com/forums/thread/118172

Return them as AnyView values:

var body: some View { 
    switch choices { 
    case .one: 
        return AnyView(MyView1())
    case .two: 
        return AnyView(MyView2())
    } 
}

State & Data Flows

State

State 속성은 현재 뷰 내에 선언된 프로퍼티가 만약 값이 변경될 때(Mutating) 뷰에 영향(Updating view)을 줄 때 사용합니다. 이 속성을 사용하게 되면 해당 프로퍼티는 SwiftUI 프레임워크에 의해 관리됩니다. (뷰 라이프사이클에 의존하지 않음) 예) 타이머 숫자, 이름을 입력받는 텍스트 필드

이 속성으로 선언된 프로퍼티의 데이터의 값 변화를 전달받기 위해서는 $ (바인딩) 을 사용하면 됩니다.

개인 의견 뷰모델을 사용하는 경우 @State 속성의 프로퍼티는 가급적 뷰모델 내부에 @Published 로 옮기는 것이 좋습니다. $username$viewModel.username 형태로 동일하게 바인딩할 수 있습니다.

@State private var username: String = ""
TextField("Enter your name", text: $username)

Binding

Binding 속성은 State 속성으로 선언된 프로퍼티의 데이터의 값 변화(mutating)을 전달받기 위해 사용합니다.

@Binding var username: String
@State var username: String = ""

var body: some View {
    ChildView(username: $username)
}

StateObject

iOS 14

StateObject 는 뷰모델을 특정 뷰가 아닌 SwiftUI 프레임워크에서 관리하도록 하고 그 모델을 참조 합니다. (Source of truth) 이 속성을 사용할 때는 선언하고자 하는 뷰모델의 클래스가 반드시 ObservableObject 를 준수하고 있어야 합니다.

@StateObject private var data = ViewModel()

뷰의 init에서 초기화 하기

iOS 14

StateObject.init(wrappedValue:) 에 뷰모델 객체를 할당하여 StateObject 를 뷰의 init 에서 초기화 할 수 있습니다.

@StateObject private var viewModel: ViewModel

init(value: SomeValue) {
    let viewModel = ViewModel(value: value)
    self._viewModel = StateObject(wrappedValue: ViewModel)
}

에러 처리하기: Accessing StateObject's object without being installed on a View

StateObject 대신 ObservedObject를 사용하도록 해서 뷰에 설치(install)되기전 뷰모델에 접근하지 않도록 한다.

StateObject는 SwiftUI 프레임워크에서 관리되며 ObsevedObject는 뷰에서 관리된다는 특징을 갖는다. 즉 StateObject는 뷰 라이프사이클과 독립적이지만 ObservedObject는 뷰라이프 사이클에 의존한다. 때문에 뷰에 설치 여부와 상관없이 StateObject에 접근할 수 있게 되고 이로인해 위와 같은 에러가 나는 것으로 추정된다. (개인의견)

ObservedObject

ObservedObject 는 뷰모델은 현재 뷰의 라이프사이클에 맞춰 사용하고 싶을 때, 즉 현재 뷰 내에서만 사용할 때 쓰이는 속성입니다. (뷰가 관리) 객체를 생성해도 되지만 이 속성의 특성상(아래의 주의 참고) 가급적이면 생성보다는 상위 뷰의 뷰모델로 부터 인스턴스를 전달받는 형태로 사용하는 것을 권장합니다. 이 속성을 사용할 때는 선언하고자 하는 뷰모델의 클래스가 반드시 ObservableObject 를 준수하고 있어야 합니다.

중요 상위 뷰가 존재하는 경우 상위 뷰가 업데이트 될때마다 현재 뷰가 재생성되어 뷰모델이 리셋 됩니다.

@ObservedObject var data: ViewModel
@StateObject private var data = ViewModel()

var body: some View {
    ChildView(data: data)
}

Published

ObservableObject 프로토콜을 준수하는 뷰모델은 @Published 속성을 사용해서 값의 변화를 퍼블리싱 하여 뷰를 업데이트 할 수 있습니다.

@Published var userID: String = ""
TextField("아이디를 입력하세요", text: $observedObject.userID)

About

SwiftUI 에 대한 내용을 끄적인 문서입니다.

Topics

Resources

Stars

Watchers

Forks