포스트

MapKit (20)

Dynamic Type

Dynamic Type에 대한 참고는 여기

간단하게 정리하면 설정에서 Dynamic Type을 통해 font size를 조절할 수 있다.

Accessibility Docs Dynamic Type sizes Docs PSDPFKit Blog - Dynamic Type Stats

여기도 참고하면 좋다.


우선 DummyData를 하나 만들어 주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
static var chipotle: CKRecord {
    let record                          = CKRecord(recordType: RecordType.location, 
                                                    recordID: CKRecord.ID(recordName: "BD731330-6FAF-A3DE-2592-677F9A62BBCA"))
    record[DDGLocation.kName]           = "Chipotle"
    record[DDGLocation.kAddress]        = "1 S Market St Ste 40"
    record[DDGLocation.kDescription]    = "Our local San Jose One South Market Chipotle Mexican Grill is cultivating a better 
                                            world by serving responsibly sourced, classically-cooked, real food."
    record[DDGLocation.kWebsiteURL]     = "https://locations.chipotle.com/ca/san-jose/1-s-market-st"
    record[DDGLocation.kLocation]       = CLLocation(latitude: 37.334967, longitude: -121.892566)
    record[DDGLocation.kPhoneNumber]    = "408-938-0919"
    
    return record
}

이때 하나 중요한게 있다. 현재 강의에 있는 그대로 MockData에 recordName을 사용했는데, 이렇게 하면 Checked In을 한 부분에 대해서는 잘 나오지 않게 된다.

그러므로, 이부분을

Image

iCloud에서 사용하는 recordName을 그대로 MockData에 사용하면 Preview에서도 보이게 된다.

그리고 Dynamic Type에 따른 Text를 보고싶을땐 지금은

Image

이걸 통해 바로 확인이 가능하다

Image

물론 코드를 통해서도 바로 확인이 가능하다

1
2
3
4
5
6
#Preview {
    NavigationView {
        LocationDetailView(viewModel: LocationDetailViewModel(location: DDGLocation(record: MockData.chipotle)))
            .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
    }
}

Modal 배경을 바꿔준다.

1
2
3
4
viewModel.isShowingProfileModal {
                Color(.black)
                // 생략
}

Modal이 보여질때 뒤의 배경을 black으로하여 검게 해주었다.

이전엔 SystemBackground였는데 비교를 해보면

Image

이렇게 차이가 난다.


이제 제대로 해보도록 한다.

1
2
3
4
5
6
7
8
9
10
11
struct DescriptionView: View {
    
    var description: String
    
    var body: some View {
        Text(description)
            .minimumScaleFactor(0.75)
            .fixedSize(horizontal: false, vertical: true)
            .padding(.horizontal)
    }
}

Description View의 frame, linelimit을 모두 지워주었다. Font를 극단적으로 키울경우에 제약이 되는 요소를 모두 제거를 했다고 보는게 맞다.

그리고 .fixedSize(horizontal: false, vertical: true)를 사용해준다.

이건 사이즈를 고정할때 쓰는 방식이다.

Image

참고

하지만 fixedSize를 사용했을때 Description부분 내용은 다 보이지만 나머지 UI가 안보이는 문제가 있다.

Majid Jabrayilov라는 개발자가 고안한 방법을 사용한다. 바로 scrollview를 embed하는 방식이다.

View Extension에 아래 함수를 추가한다.

1
2
3
4
5
6
7
func embedInScrollView() -> some View {
    GeometryReader { geometry in
        ScrollView {
            self.frame(minHeight: geometry.size.height, maxHeight: .infinity)
        }
    }
}

그리고 preview에

1
2
3
4
5
6
#Preview {
    NavigationView {
        LocationDetailView(viewModel: LocationDetailViewModel(location: DDGLocation(record: MockData.chipotle))).embedInScrollView()
            .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
    }
}

이렇게 적용하면

Image

전체가 ScrollView가 입혀져서 드래그가 가능해진다.

문제점

그럼 이제 이걸 실제로 적용하면 되지 않나? 라는 생각을 하게된다.

1
2
3
4
5
6
7
8
LocationListView

ForEach(locationManager.locations) { location in
    NavigationLink(destination: LocationDetailView(viewModel: LocationDetailViewModel(location: location)).embedInScrollView()) {
        LocationCell(location: location, profiles: viewModel.checkedInProfiles[location.id, default: []])
           // 생략
    }
}

LocationDetailView에 대해 embedInScrollView를 적용 해주었다.

Image

폰트가 적당함에도 불구하고 이렇게 전체가 그대로 ScrollView가 적용이 되는 문제가 발생한다.

해결책

1
2
3
4
5
6
7
8
9
//LocationListViewModel

@ViewBuilder func createLocationDetailView(for location: DDGLocation, in sizeCategory: ContentSizeCategory) -> some View {
    if sizeCategory >= .accessibilityMedium {
        LocationDetailView(viewModel: LocationDetailViewModel(location: location)).embedInScrollView()
    } else {
        LocationDetailView(viewModel: LocationDetailViewModel(location: location))
    }
}

LocationListViewModel에 다음과 같은 함수를 하나 만들어 주었다.

우리가 설정한 사이즈 기준에 따라 embedInScrollView를 적용할지 말지를 정하는 간단한 함수처럼 보이지만, 이때 @ViewBuilder를 사용하지 않고 만들게 되면

Image

이렇게 에러가 발생한다. some View는 함수 내의 모든 리턴 경로에서 동일한 타입을 반환해야 하는데 .embedInScrollView()를 쓴 쪽과 안 쓴 쪽은 타입이 서로 달라 충돌했기 때문

그래서 이 문제를 해결하기 위해 ViewBuilder를 사용한다.

ViewBuilder참고

  • ViewBuilder from Docs
    1. 정의: 클로저로부터 뷰를 구성(constructs)하는 커스텀 파라미터 속성이다.
    2. 핵심 역할: 자식 뷰를 생성하는 클로저 매개변수에 사용하여, 해당 클로저가 여러 개의 자식 뷰를 동시에 제공할 수 있도록 허용한다.
    3. 구현 로직:
    • 호출부(Client)는 { } 클로저 내부에 여러 개의 실행문(multiple-statement)을 나열하여 여러 뷰를 전달할 수 있다.
    • 타입 일치화: 시스템은 분기문 등에 의해 서로 다른 타입이 반환될 경우, 이를 내부적으로 _ConditionalContent와 같은 하나의 결과물(통일된 타입)로 래핑하여 some View의 단일 타입 원칙을 지켜준다.

우리가 아무렇지 않게 쓰는 Stacks(Z, H, V)들도 모두 ViewBuilder를 포함하고 있다.

Image

ZStack 설명 일부

이제 ListView에 적용을 해본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct LocationListView: View {
    // 생략
    @Environment(\.sizeCategory) var sizeCategory
    
    var body: some View {
        NavigationView {
            List {
                ForEach(locationManager.locations) { location in
                    NavigationLink(destination: 
                    viewModel.createLocationDetailView(for: location, in: sizeCategory)) {
                        // 생략
                    }
                }
                .onAppear {
                    // 생략
                }
            }
            // 생략
        }
    }
}

실행해보면 평상시엔 문제가 없다. (사진 생략)

Image

이제는 사이즈에 따라 적용이 잘 되는걸 알 수 있다.

똑같이 LocationMapView에도 적용한다.

1
2
3
4
5
6
7
.sheet(isPresented: $viewModel.isShowingDetailView) {
    NavigationView {
        viewModel.createLocationDetailView(for: locationManager.selectedLocation!, in: sizeCategory)
            // 생략
    }
    .accentColor(.brandPrimary)
}

size에 따라 Columns 다르게 하기

Image

이렇게 사이즈가 커졌을때를 대비하여 행을 관리를 해보려 한다.

1
2
3
4
5
6
7
8
9
10
11
// LocationDetailVM
func determineColumns(for sizeCategory: ContentSizeCategory) -> [GridItem] {
        let numberOfColumns = sizeCategory >= .accessibilityMedium ? 1 : 3
        return Array(repeating: GridItem(.flexible()), count: numberOfColumns)
    }

// LocationDetailView

LazyVGrid(columns: viewModel.determineColumns(for: sizeCategory)) { 
    // 생략
}

이렇게 해주었다.

그리고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct FirstNameAvatarView: View {
    
    @Environment(\.sizeCategory) var sizeCategory
    var profile: DDGProfile
    
    var body: some View {
        VStack {
            AvatarView(image: profile.createAvatarImage(),
                       size: sizeCategory >= .accessibilityMedium ? 100 : 64)
            
            // 생략
        }
    }
}

AvartarView 역시도 사이즈를 다르게 해주었다.

적용된 사진은 패스


Github: Dub-Dub-Grub Repository

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.