SwiftUI View 2 (Button, NavigationView, List, GeometryReader, Frame)

Updated:

🔷 Button

  • UIButton 역활을 하는 Button 의 역활입니다

Button의 기본구조

Button(action: {
  // 버튼 이벤트가 발생했을 때 수행할 작업
}) {
  Text("Button") // 버튼 UI
}

👉 버튼 레이블

텍스트를 이용해 버튼을 만드는 예제는 다음과 같습니다

// 버튼 생성시 외형과 이벤트 발생 시 수행할 작업에 대해 정의해야 합니다
var Example01: some View {
  HStack(spacing: 20) {

    // Button 1 : 단순히 텍스트로만 버튼을 표형할때는 title 매개 변수에 문자열을 먼저 전달하고 action 이 뒤에 옵니다
    Button("Button 1") {
      print("Button 1")
    }

    // Button 2 : 문자열만 사용하려면 button1 이지만 여러가지 decoration 을 사용하려면 2번 과 같이 action 을 먼저 정의 하고, 뷰를 나중에 정의해야 합니다
    Button(action: {print("Button 2")}) {
      Text("Button 2")
        .padding()
        .background(RoundedRectangle(cornerRadius: 10).strokeBorder())
    }

    // Button 3 : 외곽선을 먼저 그리고 그 위에 텍스트를 overlay 한 button 입니다. 여기서 accentColor 는 UIKit 에서 tintColor 의 영활을 하며, default 색은 파란색입니다
    Button(action: { print("Button 3")}) {
      Circle()
        .stroke(lineWidth: 2)
        .frame(width: 80, height: 80)
        .overlay(Text("Button 3"))
    }
    .accentColor(.green)
  }
}

스크린샷 2021-12-31 오후 9 42 36

👉 onTapGesture

  • onTabGesture 를 사용하면 버튼을 사용하지 않고도 같은 기능을 구현 할 수 있습니다
// 버튼 대신 onTapGesture 수식어를 이용하는 것도 가능합니다.
var Example04: some View {
  HStack {
    Image(systemName: "person.circle")
      .imageScale(.large)
      .onTapGesture {
        print("onTapGesture")
      }

    Button(action: {
      print("Button action")
    }) {
      Image(systemName: "person.circle")
        .imageScale(.large)
    }
  }
}

스크린샷 2022-01-01 오전 10 42 12

🔷 NavigationView

NavigationView

  • 네비게이션 뷰는 네비게이션 스택을 사용해 콘텐츠 뷰들을 관리하는 컨터이너로 UIKit 의 UINavigationController, UISplitViewController 의 역활을 수행합니다

  • 사용방법은 단순히 stack 처럼 감사 주기만 하면 됩니다

NavigationView {
  Image("SwiftUI")
}

👉 NavigationTitle

  • 네비게이션 뷰에 사용되는 수식어들은 preference 기능으로 사용되어 하위 뷰가 상위 뷰에 데이터를 전달하는 방식으로 적용됩니다
var example01: some View {
		NavigationView {
			Image("SwiftUI")
				.navigationTitle("네비게이션")
				// displaymode 로는 automatic, large, inline 등이 있습니다
				.navigationBarTitleDisplayMode(.inline)
		}
	}

스크린샷 2022-01-01 오전 11 49 32

👉 Navigation add bar items

🔶 `navigationBarItems(leading:trailing:) 을 사용해서 leading, trailing 부분에 barItems 방식은 더이상 지원하지 않고,

toolbar(content:) with navigationBarLeading or navigationBarTrailing placement. 을 사용하여 navigation bar items 를 구성해야 합니다

navigationBarItems Deprecated - https://developer.apple.com/documentation/swiftui/view/navigationbaritems(leading:trailing:)

// toolbar 를 사용해 navigationbar 추가합니다
var example02: some View {
  NavigationView {
    Image("SwiftUI")
      .navigationTitle("네비게이션 Bar items")
      .navigationBarTitleDisplayMode(.inline)
      // toolbar 사용해서 leading, trailing 에 icon 설정
      .toolbar {
        // bell item btn
        ToolbarItem(placement: .navigationBarLeading) {
          Button(action: {
            print("leading item tapped")
          }) {
            Image(systemName: "bell").imageScale(.large)
          }
        }

        ToolbarItem(placement: .navigationBarTrailing) {
          Button(action: {
            print("Trailing item tapped")
          }) {
            Image(systemName: "gear").imageScale(.large)
          }
        }
      }
  }
}

스크린샷 2022-01-01 오후 12 55 18

📌 leading 이라 trailing 위치에 둘 이상의 아이템을 넣어야 하면, ToolbarItemGroup 을 사용해서 bar items 를 그룹화 시켜 줍니다

var example03: some View {
  NavigationView {
    Image("SwiftUI")
      .navigationTitle("Navitaion BarItemGroup")
      .navigationBarTitleDisplayMode(.inline)
      .toolbar {
        // toolbarItemGroup: 2개 이상 이 있을경우
        ToolbarItemGroup(placement: .navigationBarTrailing) {
          // 1.bellBtn
          Button(action: {
            print("TapBellBtn")
          }) {
            Image(systemName: "bell").imageScale(.large)
          }

          // 2.shareBtn
          Button(action: {
            print("TapShareBtn")
          }) {
            Image(systemName: "square.and.arrow.up").imageScale(.large)
          }

          // 3.gearBtn
          Button(action: {
            print("TapGearBtn")
          }) {
            Image(systemName: "gear").imageScale(.large)
          }
        }
      }
  }
}

스크린샷 2022-01-01 오후 1 21 55

  • 네비게이션 링크는 지정한 목적지로 이동할 수 있도록 만들어진 버튼으로 뷰를 눌렀을 때 또는 특정 조건을 만족했을 때 화면을 전환합니다

  • UINavigationController 의 pushViewController 메서드를 기능을 수행하는것과 동일하며, 네비게이션 스택에 뷰를 추가하여 내비게이션 계층 구조를 형성하는데 사용됩니다

var example04: some View {
  NavigationView{
    NavigationLink(destination: Text("Destination View")) {
      Text("Destination 페이지로 이동하기")
    }
    .navigationTitle("Navigation Link")
  }
}

스크린샷

	// navigationBarHidden 을이용해 내비게이션 바를 숨길수도 있습니다
	var example05: some View {
		NavigationView{
			NavigationLink(destination: Text("Destination View")) {
				Text("Destination 페이지로 이동하기")
			}
			.navigationTitle("Navigation Link")
			.navigationBarHidden(true)
		}
	}

스크린샷

  • 네비게이션 뷰는 iOS 기준으로 3가지의 스타일을 제공하며, navigationViewStyle 을 사용해서 스타일을 명시적으로 서용할 수 있습니다.
구분 설명
DefaultNavigationStyle 네비게이션 뷰 기본 스타일. 자동으로 스타일을 결정합니다. watchOS 를 제외하고 모든 플렛폼 사용 가능
StackNavigationViewStyle 네비게이션 계층 구조를 하나의 뷰만으로 탐색해 나가는 스타일. UINavigationController 가 사용됨. iOS, tvOS 에서만 사용가능
DoubleColumNavigationViewStyle Master 와 Detail 구분되는 2개의 컬럼뷰을 이용해 컨텐츠를 표현하는 스타일. watchOS 를 제외한 모든 플랫폼에서 사용가능합니다. 내부적으로 SplitViewController 가 사용됩니다. 이 스타일을 사용할 수 없는 기기에서는 StackNavigationViewStyle 로 자동 전환 됩니다
  • 네비게이션 뷰에는 여러 개의 자식 뷰를 전달할 수 있지만 StackVNavigationViewStyle 일때는 첫번째 뷰만 인식하고 나머지는 무시하며, DoubleColumNavigationViewStyle 에 일때 첫번째 뷰와 마지막 뷰만 인식합니다
DoubleColumnNavigationViewStyle
  • UIkit 에서 UISplitViewController 는 NavigationView 에 통합되는데 사이즈 클래스의 너비가 Regular 인 기기에만 한정 됩니다.

image

  • 위의 표에서 아이폰 plus 나 max 시리즈는 가로 모드 일때 Regular mode 이고 iPad 는 세로나, 가로일때 모두 에서 Regular mode 가 된다는 점에서 이기기 들에서는 네비게이션 뷰가 SplitViewController 로서 동작해서 2개의 분활된 View로 표현된다는 점입니다
// iOS에서는 내비게이션 뷰 스타일로 Default, Stack, DoubleColumn 3가지가 제공됩니다.
// Stack은 UINavigationController
// DoubleColumn은 UISplitViewController로 동작합니다.
var example06: some View {
  NavigationView {
    VStack(spacing: 20) {
      NavigationLink(destination: Text("디테일 뷰 영역1").font(.largeTitle)) {
        Text("마스터 뷰 메뉴1").font(.title)
      }
      NavigationLink(destination: Text("디테일 뷰 영역2").font(.largeTitle)) {
        Text("마스터 뷰 메뉴2").font(.title)
      }
    }
    .navigationTitle("네비게이션 뷰 스타일")
    		Text("디테일 뷰").font(.largeTitle)
		}
		// .navigationViewStyle(StackNavigationViewStyle()) // stackNavigation 스타일로 지정
  }
}
  • 아이폰 13 max 기준

세로 모드

세로 모드는 앞서 StackNavigationViewStyle 에서의 네비게이션 뷰와 다르지 않다는 습니다. 세로 모드에서는 compact Width 에 해당되기 때문입니다

스크린샷

가로 모드 가로모드로 하면 화면에서 보여지는 영역이 달라집니다. 왼쪽에서 미는 동작을 취해주면 아래 그림처럼 가려져 있던 마스터 뷰가 나타 납니다. 그래서 사이즈 클래스에 맞춰서 적절한 스타일이 자동 반영됩니다

Kapture 2022-01-02 at 05 54 35

🔷 List

List 는 하의 여러개의 행으로 표현되는 UI를 구성해 다중 데이터를 쉽게 나열할 수 있도록 구성된 뷰입니다. UIkit 의 UITableView 와 같습은데 , 기존에는 UITableView 를 만들려면 UITableVieDataSource, UITableViewDelegate 등 을 구형해야 되는데, SwiftUI dㅔ서는 더이상 필요 없습니다

// 리스트를 이용한 뷰 표현, 여기서 뷰 하나는 로우에 해당 합니다
var example01: some View {
  List {
    Text("1")
    Text("2")
    Text("3")
    Text("4")
    Text("5")
    Text("6")
    Text("7")
    Text("8")
    Text("9")
    Text("10")
  }
}

스크린샷 2022-01-02 오전 6 10 44

📌 뷰 최대 개수 유의 사항

뷰 빌더의 제약때문에, 정적 컨텐츠 표현시 중괄호 {} 안에 10개 넘는 뷰를 집어넣으면 에러가 뜬다. ( 10개 이상을 만들려면 동적 컨텐츠를 사용해야합니다)

👉 정적 콘텐츠

리스트 생성자에 원하는 뷰를 전달하면서 하나씩 각각의 Row 에 담아 표현합니다 (UIkit 에서 뷰를 cell 이라고 했는데 여기서는 Row 라고 표현합니다)

// 여러가지 타입의 정적 콘텐츠 사용 예시
var example02: some View {
  List {
    Text("List").font(.largeTitle)
    Image("SwiftUI")
    Circle().frame(width: 100, height: 100)
    Color(.red).frame(width: 100, height: 100)
  }
}

스크린샷 2022-01-02 오전 6 30 20

그러나 리스트에 사용목적은 정적 콘텐츠 보다는 동적 콘텐츠에 있습니다

👉 동적 콘텐츠

Range

  • 동적 콘텐츠를 표현하는 첫번째 방법으로는 Range 타입의 값을 넘겨주는 것입니다
// Range<Int> 타입을 이용한 동적 콘텐츠 표현
var example03: some View {
  List(0..<100) {
    Text("\($0)")
  }
}

스크린샷

  • 여기서 범위 연산자는 Haf-open range operator(Range) 에 해당하는 `A..<B` 만 사용하는 것을 주의 해야 합니다 (`A..B` ,` A..`, `..A` 안됨)

RandomAccessCollection

두번째 방법은 RandomAccessCollection 프로토콜에 준수하는 데이터를 제공하는 것입니다. 이 경우 데이터의 각 요소들을 구분하고 식별할 수 있는 id 값을 제공해야 합니다

id 식별자 지정
  • id 로 사용할 값을 직접 인수로 제공하는 것입니다. id 매개 변수에는 Hashable 프로토콜을 준수하는 property 를 지정할 수 잇습니다. 그래서 별도의 프로퍼티를 지정하기 보다 self 라고 명시하는 것이 일반적입니다
// 사용자가 정의한 커스텀 타입에서는 Hashable 프로토콜을 채택해줘야 됩니다
private struct User: Hashable {
  let name: String
}

// RandomAccessCollection 프로토콜을 이용한 동적 콘텐츠 표현
// id 매개 변수에는 Hashable 프로토콜을 준수하는 타입의 값을 지정해 줄 수 있습니다.
var example04: some View {
  let numbers = [1, 2, 3, 4, 5]
  let alphabets = ["A", "B", "C", "D", "E"]
  let users = [User(name: "James"), User(name: "Steve"), User(name: "Edward")]
  return VStack {
    List(numbers, id: \.self) {
      Text("\(String(describing: $0))")
    }
    List(alphabets, id: \.self) {
      Text("\(String(describing: $0))")
    }
    List(users, id: \.self) {
      Text("\(String(describing: $0))")
    }
  }
}

스크린샷 2022-01-02 오전 8 45 44

identifiable 프로코콜 채택
  • 두번째 방법은 매개 변수에 id 를 전달하는 대신 데이터 타입 자체에 추가된 identifiable 프로토콜을 채택하는 것입니다. 타입 자체에 id 프로퍼티를 만들고 이것을 식별자로 삼게 됩니다.

  • identifiable 프로토콜을 준수한다면, 이미 식별자가 있으므로 리스트에 id 를 제공하지 않아도 무방합니다

// Identifiable 프로토콜을 준수하는 타입을 나열할 때는 id 매개 변수를 생략 가능합니다.
struct Example05: View {
  private struct Animal: Identifiable {
    let id = UUID()
    let name: String
  }

  var body: some View {
    List([Animal(name: "Tory"), Animal(name: "Lilly")]) {
      Text("\(String(describing: $0))")
    }
  }
}

스크린샷 2022-01-02 오전 8 57 47

👉 정적 콘텐츠와 동적 콘텐츠 조합

ForEach

  • SwiftUI 에서 ForEach 는 리스트처럼 id 로 식별할 수 있는 데이터를 받아서 동적으로 뷰를 생성하는 역활을 합니다
// ForEach 사용해서 리스트 보여주기
List {
  ForEach(0..<50) {
    Text("\($0)")
  }
}

// 같은 결과 로 Range<Int> 를 사용하기
List(0..<50) {
  Text("\($0)")
}
  • 하지만 ForEach 를 사용하면 정적 + 동적 컨텐츠 조합할 수 있습니다
List{
  Text("번호") // 하나의 로우를 차지하는 정적 뷰
  ForEach(0..<50) { // 50개의 동적 뷰 생성
    Text("\($0)")
  }
}
ForEach 사용해서 정적 + 동적 조합하기
// ForEach를 이용해 정적 콘텐츠와 동적 콘텐츠를 혼합할 수 있습니다.
struct Example06: View {
  let fruits = ["사과", "배", "포도", "바나나"]
  let drinks = ["물", "우유", "탄산수"]

  var body: some View {
    List {
      Text("Fruits").font(.largeTitle)
      ForEach(fruits, id: \.self) {
        Text($0)
      }

      Text("Drinks").font(.largeTitle)
      ForEach(drinks, id: \.self) {
        Text($0)
      }
    }
  }
}

스크린샷 2022-01-02 오전 9 08 55

👉 Section

  • 리스트는 section 을 이용해 데이터를 쉽게 그룹화 하는것도 가능합니다

  • section 에는 header, footer 를 생략하거나, 추가 할 수 있고, 둘 중 하나만 사용할 수도 있습니다.

// Section을 이용해 관련 있는 데이터들을 묶어서 표현할 수 있습니다.
struct Example07: View {
  let fruits = ["사과", "배", "포도", "바나나"]
  let drinks = ["물", "우유", "탄산수"]

  var body: some View {
    let title = ["Fruits", "Drinks"]
    let data = [fruits, drinks]
    return List {
      ForEach(data.indices) { index in // data에 포함된 횟수만큼 색션 생성
        Section(
          header: Text(title[index]).font(.title), // 헤더
          footer: HStack { Spacer(); Text("\(data[index].count)건")} // 푸터
        ) {
          ForEach(data[index], id: \.self) {
            Text($0)
          }
        }
      }
    }
  }
}

스크린샷 2022-01-02 오전 10 53 22

👉 ListStyle

iOS 기본적으로 list style 은 3가지가 있습니다

구분 설명
DefaultListStyle 리스트 기본 스타일, 사용환경에 따라 적절한 스타일을 자동으로 적용 합니다
PlainListStyle 데이터 목록을 각 행마다 하나씩 나열하는 기본 스타일. UITableView 에서 plain 을 지정하거나 생략했을 때와 같습니다. 모든 플랫폼에서 사용 가능
GroupedListStyle 각 section 을 분리된 그룹으로 묶어 표형하는 스타일. 사용환경에 따라 grouped or insetGrouped 스타일 중 하나가 자동으로 선택됩니다. iOS, tvOS 에서만 사용 가능합니다

GroupedListStyle

이전 코드의 예제에서 GroupedListStyle() 을 적용하면 다음과 같습니다. 그룹별로 더 명확히 불리가 됩니다

List {.......
}
.listStyle(GroupedListStyle())

스크린샷 2022-01-02 오전 11 07 53

insetGroupedStyle

SwiftUI 에서 사이즈 클래스가 compact 일때 는 grouped, regular 일때는 insetGroupedStyle 스타일이 적용됩니다. 아이폰 11 pro max 기준에 가로 일때와 새로 일때의 뷰의 차이가 생깁니다. (애플 휴먼 인터페이스 가이드라인에서 regular width 환경에서 insetGrouped 스타일을 사용하기 적합하는 것에 착안해 적용된것입니다)

List {.......
}
.listStyle(InsetGroupedListStyle())

스크린샷 2022-01-02 오전 10 53 22

🔷 GeometryReader

  • GeometryReader 는 자식 뷰에 부모 뷰와 기기에 대한 크기 및 좌표계 정보를 전달하는 기능을 수행하는 컨테이너 뷰입니다. 아이폰이 회전하는 경우 처럼 뷰의 크기가 변경되더라도 그 값이 자동으로 갱신 됩니다

  • 뷰가 배열되는 방식 ZStack 과 같이 겹겹이 쌓이는 계층 구조를 가집니다. 뷰가 정렬되는 방식은 topLeading 을 기준으로 배치가 됩니다.

	// 지오메트리 리더는 주어진 공간 내에서 가능한 최대 크기를 가집니다.
	// => iOS 14.0부터 뷰 하나일 때도 좌상단에 위치합니다.
	var example01: some View {
		GeometryReader { _ in
			Circle().fill(Color.purple)
				.frame(width: 200, height: 200)
				.overlay(Text("Center").font(.title))
		}.background(Color.gray)
	}
}

스크린샷 2022-01-02 오전 11 36 06

  • GeometryReader 는 크기를 지정해주지 않아도 화면 전체 크기 만큼 확장합니다. Color, Rectangle 등을 사용하는것처럼 크기를 지정해주지 않으면, 주어진 공간 내에서 최대 크기를 가지게 됩니다
// 지오메트리 리더는 주어진 공간 내에서 가능한 최대 크기를 가집니다.
// => iOS 14.0부터 뷰 하나일 때도 좌상단에 위치합니다.
var example01: some View {
  GeometryReader { _ in
    Circle().fill(Color.purple)
      .frame(width: 200, height: 200)
      .overlay(Text("Center").font(.title))
  }.background(Color.gray)
}

// 지오메트리 리더는 자식 뷰들이 ZStack과 같이 계층 구조를 이루며,
// 위치는 좌상단(TopLeading)을 기준으로 정렬됩니다.
var example02: some View {
  GeometryReader { _ in
    Circle().fill(Color.blue).frame(width: 350, height: 350)
    Circle().fill(Color.orange).frame(width: 280, height: 280)
    Circle().fill(Color.purple).frame(width: 200, height: 200)
  }.background(Color.gray)
}

스크린샷 2022-01-02 오전 11 41 33

👉 GeometryProxy

  • 지오메트리 프록시는 두개의 프로퍼티와 하나의 메서드, 하나의 첨자를 제공하여 지오메트리 리더의 레이아웃 정보를 자식뷰에 제공할 수 있습니다.
구분 설명
size 지오메트리 리더의 크기를 반환 합니다
safeAreInsets 지오메트리 리더가 사용된 환경에서의 안전 영역에 대한 크기를 반환 합니다
frame(in:) 특정 좌표계를 기준으로 한 프레임 정보를 제공합니다.
subscript(anchor:) 자식 뷰에서 anchorPreference 수식어를 이용해 제공한 좌표나 프레임을 지오메트리 리더의 좌표계를 기준으로 다시 반환하여 사용하는 첨자 입니다. 이때 Anchor 의 제네릭 매개 변수는 CGReact 또는 CGPoint 타입 두가지를 사용할 수 있습니다

Size, SageAreaInsets

지오메트리 리더와 안전 영역의 크기를 바탕으로 자식 뷰에 상대적인 크기와 위치를 지정해 줍니다. 지오메트리 리더가 사용된 환경에 따라 매개변수의 값이 달라 짐으로 결과 값이 달라집니다

// 지오메트리 프락시를 통해 부모 뷰의 크기와 안전 영역에 대한 정보를 얻을 수 있습니다.
// => iOS 14부터는 GeometryReader가 직접 안전 영역에 맞닿은 면에 한해 그 크기를 가져옵니다.
// 즉, top은 다른 뷰에 접해있고 bottom만 안전 영역에 닿아 있다면 top은 0이고 bottom만 알맞은 값을 가집니다.
var example03: some View {
  GeometryReader { geometry in
    Text("Geometry Reader")
      .font(.largeTitle).bold()
    // position 은 뷰에서의 위치를 지정
      .position(x: geometry.size.width / 2, // 지오메트리 리더 너비의 절반
                y: geometry.safeAreaInsets.top) // 상단 안전 영역 크기

    VStack {
      Text("Size").bold()
      Text("Width: \(Int(geometry.size.width))") // 지오메트리 리더 넓이 표시
      Text("Height: \(Int(geometry.size.height))") // 지오메트리 리더 높이 표시
    }
    .position(x: geometry.size.width / 2, y: geometry.size.height / 3)

    VStack {
      Text("SageAreInsets").bold()
      Text("Width: \(Int(geometry.safeAreaInsets.top))") // 상단 안전 영역 크기
      Text("Height: \(Int(geometry.safeAreaInsets.bottom))") // 하단 안전 영역 크기
    }
    .position(x: geometry.size.width / 2, y: geometry.size.height / 1.5)
  }
  .font(.title)
  .border(Color.green, width: 5)
}

스크린샷 2022-01-03 오후 1 36 51

frame

지오메트리 프록시는 프레임에 대한 정보도 제공하는데, 여기서 프레임은 단순히 그 자신의 CGReact 값을 전달하는 것이 아니라, CoordinateSpace 라는 열거형 타입이 가진 세가지 값 중 하나를 지정하면 그 좌표 공강에 관한 정보를 반환 합니다.

enum CoordinateSpace {
  case global // 화면 전체 영역(윈도우의 bounds) 를 기준으로 한 좌표 정보
  case local // 지오메트리 리더의 bounds 를 기준으로 한 좌표 정보
  case named(AnyHashable) // 명시적으로 이름을 할당한 공간을 기준으로 한 좌표 정보
}
// 지오메트리 프락시를 이용해 원하는 좌표 정보를 얻을 수 있습니다.
struct Example04: View {
  var body: some View {
    HStack {
      Rectangle().fill(Color.yellow).frame(width: 30)

      VStack {
        Rectangle().fill(Color.blue).frame(height: 200)

        GeometryReader {
          self.contents(geometry: $0)
            .position(x: $0.size.width / 2, y: $0.size.height / 2)
        }
        .background(Color.green)
        .border(Color.red, width: 4)
      }
      .coordinateSpace(name: "VStackCS") // Vstack 좌표 공간에 이름 부여
    }
    .coordinateSpace(name: "HStackCS") // Hstack 좌표 공간에 이름 부여

  }


  func contents(geometry g: GeometryProxy) -> some View {
    VStack {
      // local 은 지오메트리 자기 자신에 대한 bounds 값을 반환해서 (0, 0)이 됨
      Text("Local").bold()
      Text(stringFormat(for:g.frame(in: .local).origin)).padding(.bottom)

      // global 은 윈도우의 bounds 를 기준으로 한 좌표를 반환 합니다. Global 화면에서 표시된 (38,252) 은 윈도우 원점으로 부터 계산된 좌표를 가리킵니다
      Text("Global").bold()
      Text(stringFormat(for: g.frame(in: .global).origin)).padding(.bottom)

      // named 는 지정한 뷰으 ㅣ원점을 기준으로 한 상대적인 좌표를 구하고 싶을 때 사용합니다.
      // 부모, 조상 뷰중 미리 관심있는 뷰에 대해 cordinateSpace 에서 이름을 설정해주고 그 이름을
      // named 으 ㅣ연관 값으로 전해주면 해당 뷰의 상태적인 거리를 구할 수 있습니다.
      Text("Named VStackCS").bold()
      Text(stringFormat(for: g.frame(in: .named("VStackCS")).origin)).padding(.bottom)

      Text("Named HStackCS").bold()
      Text(stringFormat(for: g.frame(in: .named("HStackCS")).origin))
    }
  }

  // 좌표 정보 출력을 위한 method
  func stringFormat(for point: CGPoint) -> String {
    String(format: "(x: %.f, y: %.f)", arguments: [point.x, point.y])
  }
}

스크린샷 2022-01-03 오후 2 28 11

  • 지오메트리 리더는 레이아웃을 구성하는데 중요한 요소이고, SwiftUI 에서 복잡한 UI 를 구성하려면 잘 이해해야 합니다

🔷 frame

  • SwiftUI 의 수식어는 뷰를 직접 변경하는 것이 아니라 원래 뷰를 수식하는 새로운 뷰를 return 하는 개념으로 수식어를 사용합니다
Text("Frame") // Text 타입
Text("Frame").frame(width: 200) // ModifiedContent<Text, _FrameLayout>
  • 즉, Text 에서 .frame 을 추가로 해서 사용하면 ModifiedContent 가 return 되게 됩니다

👉 frame 역활

  • SwiftUI 에서 frame 은 자식뷰가 사용 가능한 크기를 제안하기 위해 사용됩니다. 뷰의 정렬 위치를 결정합니다. 제안된 공간 내에서 실제로 자식 뷰가 어느 정도의 크기를 가지고 어떻게 보이게 될지는, 그 뷰가 직접 결정하게 됩니다.
// frame 수식어는 부모 뷰가 사용할 수 있는 크기를 제안하는 기능이며,
// 실제 뷰의 크기는 자식 뷰가 스스로 결정합니다.
var example01: some View {
  VStack {
    Text("Frame")
      .background(Color.yellow) // Text 안에서의 공간만 차지
      .frame(width: 200, height: 100)

    Rectangle()
      .fill(Color.yellow)
      .frame(width: 200, height: 100) // Rectangle() 안의 범위 안에서 공간만 차지
  }
}

스크린샷 2022-01-04 오전 6 12 57

  • frame 안에서는 Alignment 타입을 가진 매개 변수가 있어 위치를 지정해 줄 수 있습니다
// alignment 매개 변수로 정렬 위치를 결정합니다
var example02: some View {
  HStack {
    Text("Frame")
      .background(Color.yellow)
      .frame(width: 100, height: 100, alignment: .leading)
      .border(Color.red)

    Text("Frame")
      .background(Color.yellow)
      .frame(width: 100, height: 100) // alignment default 는 .center
      .border(Color.red)

    Text("Frame")
      .background(Color.yellow)
      .frame(width: 100, height: 100, alignment: .trailing)
      .border(Color.red)
  }
}

스크린샷 2022-01-04 오전 6 18 41

👉 뷰 레이아웃 과정

  • SwiftUI 에서의 뷰의 레이아웃은 아래와 같은 순서로 구성됩니다

    1. 부모 뷰가 활용 가능한 크기를 자식 뷰에 제안합니다

    2. 자식 뷰는 그 자신의 크기를 결정합니다

    3. 부모 뷰는 자신의 자표 공강에서 자식 뷰를 적절하게 배치합니다

고정 크기 vs 크기 제약 조건

  • 고정 크기는 frame 에서 width, height 값을 직접 입력 해서 크기를 조절하는 방식입니다

  • 크기 제약 조건은 고정된 갑을 입력하는 대신 최소(min), 최대(max), 이상적인(ideal) 값에 대한 제약 조건을 입력하는 방법도 있습니다.

  • 단, 크기 제약 조건에서 min <= ideal <= max 오름차순으로 값을 지정해줘야 합니다

// 고정된 크기가 아닌 크기에 대한 제약조건을 설정할 수 있습니다.
var example03: some View {
  HStack {
    // minWidth vs maxWidth: 최소와 최대 너비 값에 대한 조건을 설정
    Rectangle().fill(Color.red).frame(minWidth: 100) // 최소 너비 100
    Rectangle().fill(Color.orange).frame(maxWidth: 15) // 최대 너비 15

    // height 은 항상 고정된 값을 가지지만, maxHeight 을 .infinity 로 설정하면 기기나 뷰에 따라 높이가 변화하더라도 항상 가능한 최대 높이로 설정됩니다
    Rectangle().fill(Color.yellow).frame(height: 150) // 높이를 150 고정
    Rectangle().fill(Color.green).frame(maxHeight: .infinity) // 가변적 최대 높이 설정

    // frame 에 크기를 따로 지정하지 않으면 내부적으로 .infinity 값이 default 로 설정됨
    Rectangle().fill(Color.blue).frame(maxWidth: .infinity, maxHeight: .infinity)
    Rectangle().fill(Color.purple) // 양방향으로 .infinity 를 적용한것과 같음
  }
  .frame(width: 300, height: 150)
}

스크린샷 2022-01-04 오전 6 39 18

ideal Size 와 Fixed Size

  • 이상저인 크기는 UIKit 에서의 고유 콘텐츠 크기 (Intrinsic Content Size) 와 같은 개념인데. 각각의 뷰는 그 특성에 맞는 고유 콘텐츠 크기를 가지고 있어 오토레이아웃에 활용할 수 잇습니다

  • SwiftUI 에서는 부모 뷰의 공간과 관계없이 자신에게 이상적인 크기의 값을 idealWidth, idealHeight 로 가지고 있습니다

	// fixedSize 미적용 예시
	var example04: some View {
		VStack {
			Text("Frame Modifier").font(.title).bold()
				.frame(width: 80, height: 30) // 텍스트의 실제 문자열보다 크기가 작어 size 가 작아진것
			Rectangle()
			Color.red
			Image("SwiftUI").resizable()
		}
	}
  • fixed 적용 전

스크린샷 2022-01-04 오전 6 51 16

  • fixed 적용 후

스크린샷 2022-01-04 오전 6 57 55

  • fixedSize 의 horizontal, vertical 값을 설정 해서 적용방향을 설정 할 수 있습니다
// 가로와 세로축에 대해 각각 따로 fixedSize를 적용할 수 있습니다.
var example06: some View {
  VStack (spacing: 100){
    Group { // 자식 뷰에 공통으로 동일한 수식어를 적용하고 싶을 때 Group 활용 가능
      Text("Fixed 를 적용하면 글자가 생략되지 않습니다")

      Text("Fixed 를 적용하면 글자가 생략되지 않습니다")
        .fixedSize(horizontal: true, vertical: false) // 가로축에만 적용

      Text("Fixed 를 적용하면 글자가 생략되지 않습니다")
        .fixedSize(horizontal: false, vertical: true) // 세로축에만 적용
    }
    .font(.title)
    .frame(width: 150, height: 40)
  }
}

스크린샷 2022-01-04 오전 8 06 34

Layout Priority

  • 원하는 layout 을 구현하려면 frame 뿐 아니라 layoutPriority 대해서도 알고 있어야 하는데, 레이아웃의 우선순위가 높을 경우 부모 레이아웃이 자식에 공간을 할당 하는데 있어서 형제 뷰 그룹 내에서 우선권을 가집니다.

  • 이 경우 부모 뷰의 공간이 늘어 날때, 다른 형제 뷰와 비교해 더 빨리 늘어나고, 줄어들 때는 더 늦게 줄어듭니다. 모든 뷰는 우선순위 기본값이 0으로 설정되며, 조금이라도 차이가 나도록 설정하면 값의 크기와 관계없이 우선순위가 달라집니다

var example07: some View {
  // 우선순위 미적용: 세개의 뷰가 동일한 크기로 할당되었습니다
  VStack (spacing: 20) {
    HStack {
      Color.red
      Color.green
      Color.blue
    }.frame(height: 40)

    // 우선순위 변경: red 와 blue 뷰에만 우선순위를 1로 설정하면, green 뷰는 낮은 우선순위를 가짐
    // green 이 보이지 않는데 중간에 비어 있는것은 Hstack 의 spacing 값 만큼 뷰사이에 기본으로 2개의 공간이 할당 되었기 때문에 spacing 0 으로 설정해 보면 공간이 없어짐
    HStack {
      Color.red.layoutPriority(1)
      Color.green
      Color.blue.layoutPriority(1)
    }.frame(height: 40)

    // 위와 달리 green 에 minWidth 를 30으로 설정해 주면 최소 크기를 할당 받음
    // blue 는 maxWidth 값 최대치에따라 나머지 red 부분이 확장됨을 보임
    HStack {
      Color.red.layoutPriority(1)
      Color.green.frame(minWidth: 30)
      Color.blue.frame(maxWidth: 50).layoutPriority(1)
    }.frame(height: 40)

    // 고정크기 frame 을 설정해 주면 우선순위와 상관업이 그 크기를 가집니다
    HStack {
      Color.red.frame(width: 50)
      Color.green.layoutPriority(1)
      Color.blue.frame(maxWidth: 50).layoutPriority(1)
    }.frame(height: 40)
  }.padding()
}

스크린샷 2022-01-04 오전 9 14 21


🔶 🔷 📌 🔑 👉

🗃 Reference

Apple developer official docs -

Button: https://developer.apple.com/documentation/swiftui/button

NavigationView: https://developer.apple.com/documentation/swiftui/NavigationView

list: https://developer.apple.com/documentation/swiftui/list

GeometryReader: https://developer.apple.com/documentation/swiftui/geometryreader/

SwiftUI View 의 사이즈 조절. Frame, Padding, Spacer, GeometryReader - https://unnnyong.me/2020/05/19/swiftui-view-%EC%9D%98-%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%A1%B0%EC%A0%88-frame-padding-spacer/

스윗한 SwiftUI - https://book.jacobko.info/#/book/1190014815

Categories:

Updated:

Leave a comment