# Button

버튼은 탭 이후의 동작을 action 파라미터에 클로저로 정의하고 UI 구성을 label 파라미터에서 정의한다. 레이블 뷰가 단순 텍스트로만 이루어진 경우 Button(title: StringProtocol, action: () -> Void)로 선언하면 된다. (title 파라미터에는 문자열 전달)

Button {
    value = Int.random(in: 0...100)
} label: {
    Text("Generate")
}
.padding()

// 위 아래 동일

Button("Generate") {
    value = Int.random(in: 1...100)
}
.padding()

label 파라미터에는 텍스트만 전달할 필요가 없고 이미지도 함께 전달 가능하다. SF Symbol이미지를 사용하면 텍스트뷰와 자동 정렬되어 스택뷰에 임베딩하지 않아도 된다.

Button {
    value = Int.random(in: 0...100)
} label: {
    Image(systemName: "repeat")
    Text("Generate")
}
.padding()

버튼에 둥근 모서리를 부여하기 위해서 .cornerRadius 모디파이어를 사용해도 되지만 나중의 iOS 버전에서 deprecated될 예정이기에 .clipShape 모디파이어를 사용한다.

Button {
    value = Int.random(in: 0...100)
} label: {
    Image(systemName: "repeat")
    Text("Generate")
}
.padding()
.clipShape(.rect(cornerSize: .init(width: 20, height: 20)))

직접 버튼 스타일을 정의해도 되지만 SwiftUI에서 제공하는 기본 스타일들이 있다. .buttonStyle 모디파이어를 통해 조절 가능하다.

.automatic 케이스는 버튼 뷰가 임베딩되는 컨텍스트에 맞춰 자동으로 스타일링을 진행한다. List, ContextMenu와 같은 뷰에서 사용하게 되면 버튼 스타일이 자동으로 맞춰진다.

Button("Automatic", action: {})
    .padding()
    .buttonStyle(.automatic)

.automatic 케이스 외에도 .plain, .bordered와 같은 다양한 스타일들이 존재하는데, 이러한 기본 스타일을 버튼에 적용하게 되면 .tint 모디파이어에 컬러를 전달했을때 내부적으로 미리 정의된 컬러셋이 버튼 전체에 적용된다.

Link뷰는 외부 URL로 연결할 때 사용한다. 버튼 액션에 UIApplication.shared.open 메서드를 추가하여 직접 구현해도 되지만 Link 뷰를 사용하면 뷰에 표시할 텍스트와 URL객체만 전달해주면 된다.

Button("Apple Developer") {
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
.padding()

Link("Apple Developer", destination: url)

웹사이트 URL만 적용 가능한 것이 아니라 iOS에서 제공하는 URL Scheme에 따라 자동으로 연결된다. 핸드폰 번호를 전달하면 자동으로 메신저 앱이 실행된다.

Link("Apple Developer", destination: "010-....-....")

Link.environment 모디파이어 키패스 중 \.openURL을 지정하고, 파라미터에 OpenURLAction 객체를 정의하면 URL 처리에 대한 동작을 커스텀 할 수 있다.

 Link(destination: sms) {
    Label("Apple Developer", systemImage: "house")
}
.padding()
.background(.ultraThinMaterial, in: .rect(cornerRadius: 12))
.environment(\.openURL, OpenURLAction { url in
    if 동작_1 {
        return .handled
    } else if 동작_1 {
        return .discarded
    } else if 동작_2 {
        return .systemAction(_ url: URL)
    }
    return .systemAction
})

OpenURLAction 객체의 handler 클로저 리턴타입은 OpenURLAction.Result 구조체이다.

public struct Result {
  public static let handled: OpenURLAction.Result
  public static let discarded: OpenURLAction.Result
  public static let systemAction: OpenURLAction.Result
  public static func systemAction(_ url: URL) -> OpenURLAction.Result
}

처리 로직에 따라 결과를 리턴하면 그에 맞게 링크가 동작하게 된다.

  1. .handled: 정상적으로 URL 처리가 완료된 경우를 시스템에 알리기 위함이며 딥링크 연결 및 사파리 오픈을 위한 동작을 구현한 뒤 .handled를 리턴하면 된다.
  2. .discarded: URL처리에 문제가 있음을 알리기 위함이다.
  3. .systemAction: 동작에 대한 처리를 시스템에게 위임한다. (URL 스킴 구성에 따라 내부적으로 판단됨)

SwiftUI에서 제공하는 메뉴 UI는 다음과 같이 생겼다.

6-1

메뉴 내에는 일반적으로 Button을 임베딩하고 버튼 label클로저에 Label 뷰를 임베딩하여 이미지를 추가할 수도 있다. 또한 메뉴 내에 메뉴를 계층적으로 구성할 수도 있다.

MenuprimaryAction클로저에 동작을 정의할 수 있다. primaryAction을 정의하는 경우 메뉴 탭의 기본 액션이 실행되지 않고 클로저 내의 동작이 실행된다. 메뉴를 오랫동안 탭하고 있으면 메뉴 내의 요소들이 노출되는 방식으로 동작한다.

# Toggle

Toggle 뷰는 Binding<Bool> 타입의 상태값을 받는다.

@State private var isOn = false

VStack(spacing: 30) {
    Image(systemName: isOn ? "lightbulb.fill" : "lightbulb")
        .foregroundColor(isOn ? .yellow : .gray)

    // #1
    Toggle("ON", isOn: $isOn)
        .padding()
}

토글 레이블과 컨트롤러 배치에 대한 커스텀 모디파이어가 아직은 제공되지 않기 때문에 직접 세로 배치와 같이 커스텀하고 싶으면 직접 VStack 임베딩 해야한다.

VStack {
    Toggle(isOn: $isOn, label: {
        EmptyView()
    })
    .padding()
    .labelsHidden()

    Label("On/Off", systemImage: "bolt")
}

# Slider

SliderBinaryFloatingPoint 바인딩 속성을 갖는다. in 파라미터에 값의 하한과 상한을 설정하지 않으면 기본적으로 0과 1 사이의 값으로 설정된다. label 클로저에서는 뷰를 리턴해도 iOS에서 보이지 않는다.

@State private var dragging = false

Button("Reset") {
    r = 0.0
    g = 0.0
    b = 0.0
}
.buttonStyle(.borderedProminent)
.disabled(dragging)


Slider(value: $g, in: 0...255, label: {
    EmptyView()
}, minimumValueLabel: {
    Text("G")
        .foregroundStyle(.green)
}, maximumValueLabel: {
    Text("\(Int(g))")
}, onEditingChanged: { editing in
    dragging = editing
})
.padding()

onEditingChanged클로저의 파라미터에는 드래그 여부 불리언값이 전달된다. 드래그중이면 true, 드래그를 마치면 false가 전달된다.

miminumValueLabel클로저와 maximumValueLabel 클로저에서 리턴하는 뷰는 동일해야 한다. Textframe모디파이어를 추가하면 타입 추론에 따라 View를 리턴하는 것으로 인식되어 컴파일 에러가 발생하므로 한쪽에 frame모디파이어를 추가한다면 반대쪽에도 추가해줘야 한다.

# ProgressView

진행상황을 나타내는 ProgressView는 대표적으로 Circular StyleLinear Style로 나뉜다. 따로 모디파이어를 제공하는 것은 아니고 다른 파라미터를 받는 생성자 함수를 호출하면 된다.

Circular 스타일은 익히 알고 있는 UIActivityIndicator이다. Linear Style은 선형으로 게이지가 차는 모습의 프로그레스 뷰이다.

Linear스타일은 값의 변화를 표기만 하고 능동적인 값의 처리는 하지 않기 때문에 바인딩 속성을 받는 것이 아닌 실제 값을 받는다.

ProgressView(value: progress) {
    Label("Download", systemImage: "icloud.and.arrow.down")
} currentValueLabel: {
    Text("\(Int(progress))")
}
.padding()

선형 프로그레스 뷰는 value, label, currentValueLabel을 정의하면 된다.

# Stepper

Stepper는 +, - 컨트롤러를 가지고 값을 조작해준다.

Stepper("Quantity", value: $quantity, in: 0...100, step: 1) { editing in

}
.padding()

위와 같이 @State 변수를 받는 형태로 정의해도 되고

Stepper("Quantity") {
    if quantity >= 5 {
        quantity = 0
    } else {
        quantity += 1
    }
} onDecrement: {
    if quantity <= 0 {
        quantity = 5
    } else {
        quantity -= 1
    }
}

이런 식으로 onIncrementonDecrement에서 커스텀 로직을 작성해도 된다. 레이블과 바인딩 변수를 파라미터로 받는 경우 value와 step은 기본값이 설정되어 있다.

# Picker

Picker를 통해 피커뷰를 구현할 수 있다. Identifiable, CaseIterable 프로토콜을 채택한 열거형을 전달하거나 Identifiable로 각 값을 구분 가능한 아이디 속성이 있으면 된다.

selection파라미터에 각 값을 바인딩 속성으로 전달하면 된다.

Picker("Favorite", selection: $selected) {
    Text("Soccer").tag(Sports.soccer)
    Text("Baseball").tag(Sports.baseball)
    Text("Baseketball").tag(Sports.basketball)
}

이런식으로 내부에 요소 여러개를 전달할때 각 row당 표시할 데이터를 고유하게 구분해야 하므로 구분되는 selection으로 전달되는 고유 값을 tag에 전달하면 된다.

요소가 많아지는 경우 매번 tag속성에 값을 직접 전달하는 것이 번거롭기 때문에 ForEach를 통해 간단히 구현 가능하다. ForEach에 전달된 데이터 모델이 Identifable을 채택하고 있으면 id값을 통한 구분이 자동으로 이루어지므로 반복된 로직을 직접 작성할 필요가 없다.

Picker("Favorite", selection: $selected) {
    ForEach(Sports.allCases) { item in
        Text(item.rawValue)
    }
}
.pickerStyle(.wheel)

또한 피커를 List에 임베딩하면 컨텍스트에 맞춰 자동으로 UI가 조정된다.

List {
    Text(selected.rawValue)
        .font(.system(size: 200))

    Picker("Favorite", selection: $selected) {
        ForEach(Sports.allCases) { item in
            Text(item.rawValue)
        }
    }
    .pickerStyle(.inline)
}

# DatePicker

DatePicker("날짜 선택", selection: $selectedDate, displayedComponents: [.date, .hourAndMinute])
                .padding()
                .datePickerStyle(.wheel)
                .labelsHidden()

DatePicker로 데이터피커를 구현할 수 있다. 피커뷰와 유사하게 selectionDate타입 바인딩 속성을 전달한다. displayedComponents에 값을 전달하여 데이트피커 표시 데이터를 지정할 수 있다.

datePickerStyle 모디파이어로 명시적으로 데이트피커 스타일을 지정할 수 있다. OS별로 표시 기본 스타일이 달라질 수 있으므로 명시적으로 표시해두는게 좋다.

wheel의 경우 레이블이 찌그러질 수 있으므로 레이블은 숨기는게 좋다.

# ColorPicker

ColorPicker로 컬러피커를 구현할 수 있다.

ColorPicker("컬러 선택", selection: $backgroundColor, supportsOpacity: true)

Color타입의 바인딩 속성을 selection에 전달하면 된다. UI 커스텀은 불가능하고 레이블 labelIsHidden 모디파이어만 적용 가능하다.

# Reference

  1. Handling links with SwiftUI's openURL (opens new window)
  2. Hacking with swift - How to customize the way links are opened (opens new window)