SwiftUIでPageControlの色を1ページ毎変更する方法

2022.10.22

SwiftUIでUIPageViewControllerのようなページめくりのViewを作成した際に、そのPageControlの色を1ページ毎変更する方法を調べました。

環境

  • Xcode 14

ページめくりのViewを作成する

SwiftUIでUIPageViewControllerのようなページめくりを表現するには、TabViewとiOS 14から使用できる.tabViewStylePageTabViewStyleを組み合わせることによって簡単に表現することが出来ます。

struct ContentView: View {

    @State private var selection = 0

    var body: some View {

        TabView(selection: $selection) {

            Group {
                Text("0")
                    .tag(0)

                Text("1")
                    .tag(1)

                Text("2")
                    .tag(2)
            }
            .font(.largeTitle)
            .foregroundColor(.white)
        }
        .background(.black)
        .tabViewStyle(.page(indexDisplayMode: .automatic))
    }
}

Demo 1

PageTabViewSyle

.tabViewStylePageTabViewSyleを設定することでページスクロールするTabViewを表現することが出来ます。

また、.page(indexDisplayMode:)PageTabViewStyle.IndexDisplayModeを指定すると、インデックスの表示モードを変更出来ます。

PageTabViewStyle.IndexDisplayMode

  • always
    • ページ数に関係なく常にインデックス ビューを表示する
  • automatic
    • 複数のページがある場合はインデックス ビューを表示します
  • never
    • インデックス ビューを表示しない

PageControlのcurrentPageIndicatorTintColorがデフォルトで白という問題

簡単にページめくりの表現は出来たのですが、PageControlcurrentPageIndicatorTintColorがデフォルトで白色という問題が出てきました。

背景が白色のViewの場合はPageControlがほとんど見えないです。

PageControlの色を変更する

調べたところ、現状ではPageControlの色を変更するには、UIPageControlUIAppearanceの値を直接変更する必要がありそうでした。

struct ContentView: View {

    @State private var selection = 0

    // 初期化時にUIPageControlの色を変更
    init() {
        let currenTintColor = UIColor.black
        UIPageControl.appearance().currentPageIndicatorTintColor = currenTintColor
        UIPageControl.appearance().pageIndicatorTintColor = currenTintColor.withAlphaComponent(0.2)
    }

    var body: some View {

        TabView(selection: $selection) {

            Group {
                Text("0")
                    .tag(0)

                Text("1")
                    .tag(1)

                Text("2")
                    .tag(2)
            }
            .font(.largeTitle)
        }
        .tabViewStyle(.page(indexDisplayMode: .automatic))
    }
}

初期化時に色を変更

init() {
    let currenTintColor = UIColor.black
    UIPageControl.appearance().currentPageIndicatorTintColor = currenTintColor
    UIPageControl.appearance().pageIndicatorTintColor = currenTintColor.withAlphaComponent(0.2)
}

初期化時にUIPageControl.appearance()に変更したい色を渡すことでUIPageControlの色を変更出来ました。

ただ問題点としては、UIPageControl.appearance()の値を変更すると、アプリ全体のUIPageControlの色に影響を与えてしまいます。

PageControlの色を1ページ毎変更する

UIPageControlUIAppearanceを変更する方法を用いて、1ページ毎に色を変更する方法を試してみました。

SelectionTypeを作成

先ほどまではselectionInt型を使用していましたが、選択されたものによって、foregroundColorbackgroundColorを変更したかったのでSelectionTypeというenumを作成しました。

enum SelectionType: Int, CaseIterable, Identifiable {
    case first = 0
    case second = 1
    case third = 2

    var id: Int {
        return self.rawValue
    }

    var foregroundColor: Color {
        switch self {
        case .first, .third:
            return .white
        case .second:
            return .black
        }
    }

    var backgroundColor: Color {
        switch self {
        case .first, .third:
            return .black
        case .second:
            return .white
        }
    }
}

SelectionViewを作成

渡されたSelectionTypeによってUIPageControl.appearance()を変更したかったのでカスタムViewを作成しました。

struct SelectionView: View {

    init(type: SelectionType) {
        self.number = type.id

        // UIPageControlの色を変更する
        let currentPageIndicatorTintColor = UIColor(type.foregroundColor)
        UIPageControl.appearance().currentPageIndicatorTintColor = currentPageIndicatorTintColor
        UIPageControl.appearance().pageIndicatorTintColor = currentPageIndicatorTintColor.withAlphaComponent(0.2)
    }

    let number: Int

    var body: some View {
        Text(String(number))
    }
}

ContentView

selectionTypeによって、foregroundColorbackgroundColorを変更しています。

struct ContentView: View {

    @State private var selectionType: SelectionType = .first

    var body: some View {

        TabView(selection: $selectionType) {

            ForEach(SelectionType.allCases) { type in
                SelectionView(type: type)
                    .tag(type)
                    .font(.largeTitle)
                    .foregroundColor(type.foregroundColor)
            }
        }
        .tabViewStyle(.page(indexDisplayMode: .automatic))
        .background(selectionType.backgroundColor)
    }
}

しかし、init()で値を更新する方法では、Bindingで色を変更しているわけではない為、背景色が白の際もcurrentPageIndicatorTintColorが白のままになっていて、よく見えない結果になりました。

Demo 2

対応策

PageControl部分を自作して、SelectionTypeによって色を変更します。

SectionPageControl

struct SelectionPageControl: UIViewRepresentable {

    @Binding var type: SelectionType
    let numberOfPages: Int

    func makeUIView(context: Context) -> UIPageControl {

        let control = UIPageControl()
        // PageControl上に表示するページ数を指定する
        control.numberOfPages = numberOfPages

        // PageControlの現在のページ
        control.currentPage = type.id

        let currentIndicatorTintColor = UIColor(type.foregroundColor)
        control.currentPageIndicatorTintColor = currentIndicatorTintColor
        control.pageIndicatorTintColor = currentIndicatorTintColor.withAlphaComponent(0.2)

        // PageControl自体をタップされてもPageControl自体が動作しないようにする
        control.isUserInteractionEnabled = false
        return control
    }

    func updateUIView(_ uiView: UIPageControl, context: Context) {

        // PageControlの現在のページ
        uiView.currentPage = type.id

        let currentIndicatorTintColor = UIColor(type.foregroundColor)
        uiView.currentPageIndicatorTintColor = currentIndicatorTintColor
        uiView.pageIndicatorTintColor = currentIndicatorTintColor.withAlphaComponent(0.2)
    }
}

selectionTypeをBindingして、その値の変更をcurrentPageIndicatorTintColorに適用しています。

不要になったUIPageControl.appearanceを消す

struct SelectionView: View {

    init(type: SelectionType) {
        self.number = type.id

// 不要になった為、消す
//        let currentPageIndicatorTintColor = UIColor(type.foregroundColor)
//        UIPageControl.appearance().currentPageIndicatorTintColor = currentPageIndicatorTintColor
//        UIPageControl.appearance().pageIndicatorTintColor = currentPageIndicatorTintColor.withAlphaComponent(0.2)
    }

    let number: Int

    var body: some View {
        Text(String(number))
    }
}

ContentViewに自作PageControlを設置する

struct ContentView: View {

    @State private var selectionType: SelectionType = .first

    var body: some View {

        // VStackで囲む
        VStack {

            TabView(selection: $selectionType) {

                ForEach(SelectionType.allCases) { type in
                    SelectionView(type: type)
                        .tag(type)
                        .font(.largeTitle)
                        .foregroundColor(type.foregroundColor)
                }
            }
            // TabView側のPageControlを.neverに変更
            .tabViewStyle(.page(indexDisplayMode: .never))

            // 自作PageControlを設置
            SelectionPageControl(type: $selectionType,
                                 numberOfPages: SelectionType.allCases.count)
        }
        // VStackのbackgroundを変更
        .background(selectionType.backgroundColor)
    }
}
  1. SelectionPageControlを設置して、ページ数とバインディングするSelectionTypeを渡します。
  2. TabView側のPageControlは不要になる為、indexDisplayMode.neverに変更します。
  3. 自作PageControlを画面下部に配置する為にVStackTabViewSelectionPageControlを囲みます
  4. VStackbackgroundColorを希望のカラーの変更します。

結果

無事にページ毎にPageControlの色を変更することが出来ました!

Demo 3

おわりに

今回はTabViewを使う前提でUIPageControlを自作する方法で対応しましたが、UIPageViewControllerUIViewControllerRepresentableで表現する方法もあるかなと思いました。

できれば力技で表現する方法は避けたいのでTabViewindexDisplaytintColorを気軽に変更できるようなAPIが追加される日を楽しみにしております。

より良い変更方法がありましたら、優しく教えていただけると幸いです。

参考