SH Lab の アプリ開発部屋

リリースしたアプリの告知とかサポートとか、技術的なお話とか、そんな感じでつぶやきます。

SwiftUI でYoutube動画プレイヤーを作ってみる

はじめに

SwiftUIでアプリを作っていても、どうしてもUIKitの資産を使いたい場面が出てくる事が多いですよね。UIKitむけの豊富なライブラリたちを使いたい場合などもたくさんあると思います。

今回はまさに、UIKitむけのYoutubeプレイヤーライブラリである YoutubeKit をSwiftUIのアプリに組み込んでみたいと思います。

github.com

とりあえずの完成形

今回はこんな感じのものを作ってみたいと思います。

f:id:hoshi0523:20201118112427g:plain

GitHubはこちらです github.com

開発環境

参考書

SwiftUIの勉強には、この辺りの本が特におすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI

YoutubeKitを取り込む

今回はcocoapodsを使って、YoutubeKitを組み込んでいます。Swift Package Manager を使いたかったのですが、なぜかビルドが通りませんでした...

pod 'YoutubeKit'

UIViewRepresentable

UIViewRepresentableを使う事で、UIKitベースのViewをSwiftUIで利用可能なViewにラッピングできます。

今回は、YoutubeKitに含まれるYTSwiftyPlayerを、SwiftUIで利用可能なPlayerViewとしてラップします。

import SwiftUI
import YoutubeKit

struct PlayerView: UIViewRepresentable {
    
    typealias UIViewType = YTSwiftyPlayer
    
    // 初期化処理を記述.
    func makeUIView(context: Context) -> YTSwiftyPlayer {
        // 初期化処理・初期設定.
        let player = YTSwiftyPlayer()
        player.autoplay = true
        return player
    }
    
    // 更新処理を記述.
    func updateUIView(_ uiView: YTSwiftyPlayer, context: Context) {
        // 再生する動画の設定.
        uiView.setPlayerParameters([
            .playsInline(true),
            .videoID("QW2TfV20FXY"),
        ])
        uiView.loadPlayer()
    }
}

makeUIViewには、対象のUIViewを生成・初期化するための処理を記述します。今回はYTSwiftyPlayerインスタンスの生成を行っています。

updateUIViewには、PlayerViewが更新されるたびに呼び出される処理を記述します。今回は、再生する動画の設定と読み込み処理を行っています。事前に自動再生を有効にしているので、読み込みが終わり次第再生が開始されます。

ContentViewでこのPlayerViewを読み込んで動きを確認してみます。

struct ContentView: View {
    var body: some View {
        PlayerView()
    }
}

出来上がりはこんな感じです。画面いっぱいにPlayerViewが広がって、固定で動画が再生されていますね。

f:id:hoshi0523:20201118114313g:plain

プレイヤーの大きさを調整

このままだとプレイヤーの見た目が残念なので、ContentViewに色々と処理を追加し、PlayerViewのサイズを調整してみます。

プレイヤーの幅はデバイス幅に合わせ、高さは16:9になるように算出します。

@State private var playerSize: CGSize = .zero

var body: some View {
    VStack {
        PlayerView()
            .frame( // 算出された値でサイズ指定.
                width: playerSize.width,
                height: playerSize.height
            )
    }
    // onAppearでサイズの計算を行う.
    .onAppear {
        // windowサイズからplayerサイズを算出.
        let frame = UIApplication.shared.windows.first?.frame ?? .zero
        
        // 縦横比が16:9になるように高さを調整.
        playerSize = CGSize(
            width: frame.width,
            height: frame.width / 16 * 9
        )
    }
}

それらしくなりましたね。

f:id:hoshi0523:20201118115738g:plain

動画の切り替え機能を追加する

今は再生する動画が固定になっていますが、これを切り替えるための機能を追加します。

まずは切り替え対象となる動画の種類をenumVideoとして定義します。

enum Video: String, CaseIterable {
    
    case guitar
    case nirvana
    case amaiyume
    
    var videoId: String {
        switch self {
        case .guitar: return "QW2TfV20FXY"
        case .nirvana: return "hTWKbfoikeg"
        case .amaiyume: return "Y-ou8d-wMoI"
        }
    }
}

ContentViewには、選択された動画を保持する変数と動画切り替えのUIを追加します。

// 選択された動画を保持する変数を追加
@State private var selected: Video = .guitar

var body: some View {
    VStack {
    
        // ~省略~

        // 動画切り替えのUIを追加.
        Picker("動画選択", selection: $selected) {
            ForEach(Video.allCases, id: \.self) { video in
                Text(video.rawValue.uppercased())
            }
        }
        // picker style 色々.
//        .pickerStyle(InlinePickerStyle())
        .pickerStyle(SegmentedPickerStyle())
//        .pickerStyle(MenuPickerStyle())
    }
    .onAppear {
        // ~省略~
    }
}

今回はPickerSegmentedPickerStyle()で表示しましたが、iOS14からstyleが追加されているので、それらを試してみるのも面白いかもしれません。

  • InlinePickerStyle()
  • MenuPickerStyle()

選択された動画を再生する

切り替えのUIは作ったので、実際に動画を切り替える処理を追加します。

PlayerViewには、選択された動画のVideo@Bindingの変数として定義し、その値を利用して動画を再生するように修正します。

// 選択されたVideoを受け取る@Binding変数を追加.
@Binding var video: Video

// ~省略~

func updateUIView(_ uiView: YTSwiftyPlayer, context: Context) {
    // 再生する動画の設定.
    uiView.setPlayerParameters([
        .playsInline(true),
        .videoID(video.videoId), // 選択された値のvideoIdを利用する.
    ])
    uiView.loadPlayer()
}

ContentViewでは、PlayerViewに選択されたVideoをbindingで渡します。

// コンストラクタに値を追加.
PlayerView(video: $selected)
    .frame(
        width: playerSize.width,
        height: playerSize.height
    )

これで完成です!出来上がりはこんな感じです!

f:id:hoshi0523:20201118112427g:plain

Youtube Data API

YoutubeKitには、他にもYoutube Data API を使うための機能など、他にもさまざまな機能が実装されています。今回は解説しませんが、これらと組み合わせる事でこんな感じのアプリも作れます!

f:id:hoshi0523:20201118122420g:plain

まとめ

UIViewRepresentableを利用することで、UIKitの資産を使ったSwiftUIアプリを作る事ができます。先人たちによって作られた様々なライブラリやツールを使って、SwiftUIのアプリを作っていきましょう!

おまけ

今回使ったYoutube動画はこちらです。あまい夢、最高


A Metal Tribute to iPhone Ringtones || ToxicxEternity


Nirvana - Smells Like Teen Spirit (Official Music Video)


上田麗奈「あまい夢」 MUSIC VIDEO

SwiftUI のPathとカスタムシェイプで独自のタブバーを作ってみる

カスタムシェイプとアニメーション

SwiftUIでは、Circle()Capsule()のように初めから用意されているシェイプの他にも、Shapeプロトコルを実装して独自にシェイプを作成することができます。

さらには、そのシェイプにアニメーションを与えることもできるので、早速試してみたいと思います。

とりあえずの完成形

今回はこんな感じのものを作ってみたいと思います。 f:id:hoshi0523:20201103121707g:plain

GitHubはこちらです github.com

開発環境

参考書

SwiftUIの勉強には、この辺りの本が特におすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI

CoreGraphicsの勉強は、この辺りがよろしいかと思います。

タブ部分の作成

まずはタブの要素を表すenumを定義しています。

enum TabItem: String, CaseIterable {
    
    case home
    case favorite
    case settings
    
    var imageName: String {
        switch self {
        case .home: return "house"
        case .favorite: return "suit.heart"
        case .settings: return "gearshape"
        }
    }
}

このenumを使って、タブ部分の見た目を作成します。

VStack {
    // tab view.
    HStack {
        
        Spacer()
        
        ForEach(TabItem.allCases, id: \.self) { tabItem in
            Button(action: {}, label: {
                Text(tabItem.rawValue.uppercased())
                    .fontWeight(.heavy)
                    .foregroundColor(.white)
            })
            .frame(width: 100, height: 100)
        }
        
        Spacer()
    }
    .frame(height: 70)
    .background(
        Color.accentColor
            .ignoresSafeArea()
    )
    
    // TODO: content body.
    Spacer()
}

出来上がりはこんな感じです。タブをタップしても何も起きませんね。

f:id:hoshi0523:20201103123241p:plain

タブの位置を取得する

タップされたタブの位置情報を取得するための処理を追加します。まずはその位置情報を保持するためのプロパティを@Stateで定義します。

// 選択中のタブのx軸中央.
@State private var tabMidX: CGFloat = 0

次に、ボタンそれぞれをGeometryRenderで囲い、位置情報などを取得可能にします。

ForEach(TabItem.allCases, id: \.self) { tabItem in
    // 1. GeometryReaderでボタンを囲う.
    GeometryReader { geometry in

        Button(action: {
            // 2. タップされたボタンのx軸中央を保持.
            tabMidX = geometry.frame(in: .global).midX
        }, label: {
            Text(tabItem.rawValue.uppercased())
                .fontWeight(.heavy)
                .foregroundColor(.white)
        })
        // 3. frameはGeometryReaderに従う.
        .frame(
            width: geometry.size.width,
            height: geometry.size.height
        )
        .onAppear {
            // 4. 初期表示時のみ、一番左のタブのポジションを保持.
            if tabItem == TabItem.allCases.first {
                tabMidX = geometry.frame(in: .global).midX
            }
        }
    }
    // 5. frameはGeometryReaderで定義.
    .frame(width: 100, height: 100)
}
  1. GeometryRenderを利用して、ボタンを囲っています
  2. ボタンがタップされたら、そのボタンのX軸方向の中心座標を保持します
  3. frameには直接数値を与えるのではなく、親ビューの値を利用するようにしています
  4. 画面表示のタイミングで、一番左のタブの座標情報を初期値として保持しています
  5. GeometryRenderframe情報を与えています

保持している値を確認できるように、デバッグ用のTextを追加しています

    // ~ 省略 ~

    // TODO: content body.
    Spacer()

    // debug.
    Text("\(tabMidX)")
}

できあがりはこんな感じです。
選択されたタブのポジションが取得できているようですね!

f:id:hoshi0523:20201103125252g:plain

簡単なカスタムシェイプを作ってみる

ではカスタムシェイプを作ってタブ部分に適用してみます。

まずは既存のシェイプを返すだけのカスタムシェイプを作ってみます。カスタムシェイプは、Shapeを実装した構造体を作成し、pathというメソッドを実装します。

struct TabShape: Shape {
    func path(in rect: CGRect) -> Path {
        // とりあえずCapsuleを返すだけ.
        Capsule().path(in: rect)
    }
}
.background(
    Color.accentColor
        // 作成したシェイプでclipShapeを行う.
        .clipShape(TabShape())
        .ignoresSafeArea()
)

出来上がりはこんな感じです。見事にCapsuleでクリップされましたね。

f:id:hoshi0523:20201103125849p:plain

独自のカスタムシェイプを作ってみる

では改めてカスタムシェイプを作成していきます。まずは基本となる矩形を描画します。

func path(in rect: CGRect) -> Path {
    Path { path in
        // 基本となる矩形.
        path.move(to: .zero)
        path.addLine(to: CGPoint(x: 0, y: rect.height))
        path.addLine(to: CGPoint(x: rect.width, y: rect.height))
        path.addLine(to: CGPoint(x: rect.width, y: 0))
        path.closeSubpath()
    }
}

次に、カスタムシェイプにタブの位置情報のためのプロパティを定義します。

struct TabShape: Shape {
    // タブのX軸方向の中心座標.
    var tabMidX: CGFloat

    // ~ 省略 ~
}

呼び出し側にも修正を加えます。

.background(
    Color.accentColor
        // TabShapeの引数にtabMidXを指定.
        .clipShape(TabShape(tabMidX: tabMidX))
        .ignoresSafeArea()
)

曲線を描画する

曲線のサイズやそれぞれのコントロールポイントのポジションがどのように算出されているかは、下図を参考にしてください。

f:id:hoshi0523:20201103133241p:plain

では実際に曲線を描画します。

Path { path in
    
    // ~省略~
    
    let curveWidth = rect.width / 10
    let curveHeight = rect.height / 5
    
    // 始点.
    path.move(to: CGPoint(x: tabMidX - curveWidth, y: rect.height))
    
    // 左半分.
    let to1 = CGPoint(x: tabMidX, y: rect.height - curveHeight)
    let ctrl1a = CGPoint(x: tabMidX - (curveWidth / 2), y: rect.height)
    let ctrl1b = CGPoint(x: tabMidX - (curveWidth / 2), y: rect.height - curveHeight)
    path.addCurve(to: to1, control1: ctrl1a, control2: ctrl1b)
    
    // 右半分.
    let to2 = CGPoint(x: tabMidX + curveWidth, y: rect.height)
    let ctrl2a = CGPoint(x: tabMidX + (curveWidth / 2), y: rect.height - curveHeight)
    let ctrl2b = CGPoint(x: tabMidX + (curveWidth / 2), y: rect.height)
    path.addCurve(to: to2, control1: ctrl2a, control2: ctrl2b)
}

出来上がりはこんな感じです。選択されたボタンに合わせて、カスタムシェイプが描画されていますね。

f:id:hoshi0523:20201103133918g:plain

カスタムシェイプにアニメーションを与える

カスタムシェイプにアニメーションを与えるには、カスタムシェイプ内にanimatableDataを定義する必要があります。アニメーションを伴って変化させたいプロパティを、以下のように記述します。

struct TabShape: Shape {
    
    var tabMidX: CGFloat
    
    // アニメーションを伴って変化させたい値を、getter / setter で定義する
    var animatableData: CGFloat {
        get { tabMidX }
        set { tabMidX = newValue }
    }

    // ~省略~

ボタンタップ時の処理は、withAnimationを使って囲います。今回はボヨヨン感を出すため、interactiveSpringを利用しています。

Button(action: {
    // アニメーションを伴って変化させる.
    withAnimation(
        .interactiveSpring(
            response: 0.5, dampingFraction: 0.5, blendDuration: 0.5
        )
    ) {
        // タップされたボタンのx軸中央を保持.
        tabMidX = geometry.frame(in: .global).midX
    }
}, label: {
    // ~省略~

出来上がりはこんな感じです。アニメーションを伴って変化するようになりましたね!

f:id:hoshi0523:20201103135012g:plain

それぞれのタブにアイコンを加える

では、選択中のタブにアイコンを表示してみます。まずは選択中のタブを保持するためのプロパティを@Stateで定義します。

// 選択中のタブ.
@State private var selected: TabItem = .home

それぞれのタブボタンに、アイコン画像を与えます。ZStateを利用して重ねるようなイメージで配置します。

ForEach(TabItem.allCases, id: \.self) { tabItem in
    
    ZStack {
        // アイコン画像を追加.
        Image(systemName: tabItem.imageName)
            .foregroundColor(.accentColor)
            // 選択状態で、Y軸方向のポジションを切り替える.
            .offset(y: tabItem == selected ? 26 : -100)
        
        GeometryReader { geometry in
            // ~省略~
    }
}

出来上がりはこんな感じです。選択されたタブに合わせて、アイコン画像がアニメーションを伴って現れるようになりました!

f:id:hoshi0523:20201103140044g:plain

コンテンツ部分のViewを切り替える

では、タブビューらしくメイン領域を切り替えられるように実装します。メイン領域にはダミーのビューを配置します。

// コンテンツ領域のダミービューを定義する.
struct ContentBodyView: View {
    
    let tabItem: TabItem
    
    var body: some View {
        Text(tabItem.rawValue.uppercased())
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.accentColor)
    }
}

TabViewを利用してメイン領域にビューを配置します。アイコンが裏に回ってしまわないよう、zIndexを利用してZ軸方向の順番を調整しています。

// ~省略~
}
.frame(height: 70)
.background(
    Color.accentColor
        .clipShape(TabShape(tabMidX: tabMidX))
        .ignoresSafeArea()
)
.zIndex(1) // Z軸的に手前に描画する.

// TabViewを利用して、メイン領域にビューを配置する.
TabView(selection: $selected) {
    ForEach(TabItem.allCases, id: \.self) { tabItem in
        ContentBodyView(tabItem: tabItem)
            .tag(tabItem)
    }
}
.zIndex(0) // Z軸的に奥に描画する.

タブビューは不要なので、イニシャライザを利用して非表示にしています。

// ~省略~

// 選択中のタブ.
@State private var selected: TabItem = .home

init() {
    UITabBar.appearance().isHidden = true
}

// ~省略~

ステータスバーやドロップシャドウの調整

Info.plistView controller-based status bar appearanceを調整して、ステータスバーの色をlightにしています。

また、タブビューにドロップシャドウを追加しています。

// ~省略~
.background(
    Color.accentColor
        .clipShape(TabShape(tabMidX: tabMidX))
        .ignoresSafeArea()
        // ドロップシャドウを追加.
        .shadow(
            color: Color.black.opacity(0.1),
            radius: 1, x: 0, y: 5
        )
)
.zIndex(1)

これで完成です!出来上がりはこんな感じです!

f:id:hoshi0523:20201103121707g:plain

まとめ

カスタムシェイプを使えば、独自の計上の背景などをたやすく作ることができます!アニメーションを与えることもできますので、色々と試してみてください。

今回作ったアプリのGitHubはこちらです。よかったら参考にしてみてください! github.com

SwiftUI 2.0 でカスタムタブビューを作ってみた

SwiftUI 2.0で追加されたAPI

SwiftUI 2.0 で追加されたPageTabViewStyleを使ってみたかったので、カスタムタブのようなものを作ってみました。

とりあえずの完成形

こういったものを作っていこうと思います。 999.gif

GitHubはこちらです。 github.com

開発環境

参考書

SwiftUIの勉強には、この辺りの本が特におすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI

事前準備

タブ部分の作成

スクリーンショット 2020-10-21 21.12.23.png

アニメーションgifファイルはこのように名前をつけて配置したので、名前を合わせる形でenumを定義しています。

enum TabItem: String, CaseIterable {
    case piyo
    case pen
    case neko
    case tobipen
    
    var name: String {
        "\(self.rawValue).gif"
    }
}

タブの一つ一つを表すためのTabItemViewを追加して、以下のように定義しました。

struct TabItemView: View {
    
    let tabItem: TabItem
    @Binding var selected: TabItem
    
    var body: some View {
        // SDWebImageSwiftUIのimportが必要.
        AnimatedImage(name: tabItem.name)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40)
            .onTapGesture {
                selected = tabItem // タップしたら自身をselectedに.
            }
    }
}

メインとなるContentViewには、以下のようにタブビューを定義しました。

struct ContentView: View {
    
    // タブの選択値と初期値.
    @State private var selected: TabItem = .piyo
    
    var body: some View {
        
        // タブビュー部分.
        HStack {
            ForEach(TabItem.allCases, id: \.self) { tabItem in
                TabItemView(tabItem: tabItem, selected: $selected)
            }
        }
        .padding(.vertical, 10.0)
        .padding(.horizontal, 20.0)
        .background(Color.white.clipShape(Capsule()))
        .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
    }
}

出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。 001.gif

選択状態がわかるように見た目を調整する

選択時/非選択時で見た目を切り替えるため、TabItemViewを以下のように書き換えます。

  • frameを調整
  • paddingを調整
  • offsetを調整
  • タップ時の処理にアニメーションを伴わせる
var body: some View {
   AnimatedImage(name: tabItem.name)
       .resizable()
       .aspectRatio(contentMode: .fit)
       // 選択状態によって、サイズや間隔を調整する.
       .frame(width: tabItem == selected ? 100 : 40)
       .padding(.vertical, tabItem == selected ? -30 : 0)
       .padding(.horizontal, tabItem == selected ? -14 : 16)
       .offset(y: tabItem == selected ? -15 : 0)
       .onTapGesture {
           withAnimation(.spring()) {
               selected = tabItem // タップしたら自身をselectedに.
           }
       }
}

見た目はこのようになります。選択状態が一目でわかるようになりました。 002.gif

背景色の設定と、タブの配置調整

ContentViewの見た目を調整します。

  • 全体をZStackで囲う
  • 最背面にColor("bg").ignoresSafeArea()を配置して背景色とする
  • タブビュー部分をVStackSpacerを利用して画面下部に配置
var body: some View {
    
    ZStack {
        
        // 背景色.
        Color("bg").ignoresSafeArea()
        
        VStack {
            
            Spacer(minLength: 0)
            
            // タブビュー部分.
            HStack {
                ForEach(TabItem.allCases, id: \.self) { tabItem in
                    TabItemView(tabItem: tabItem, selected: $selected)
                }
            }
            .padding(.vertical, 10.0)
            .padding(.horizontal, 20.0)
            .background(Color.white.clipShape(Capsule()))
            .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
        }
    }
}

見た目はこうなりました。それっぽくなってきましたね。 003.gif

画面をタブで切り替える

タブは用意したので、このタブに連動して画面が切り替わるようにします。

まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。

struct HomeView: View {
    var body: some View {
        Text("Home")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.red)
    }
}

struct ListView: View {
    var body: some View {
        Text("List")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.green)
    }
}

struct SearchView: View {
    var body: some View {
        Text("Search")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.blue)
    }
}

struct SettingView: View {
    var body: some View {
        Text("Setting")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.yellow)
    }
}

最後に、これらのViewをTabViewで定義し、カスタムタブと連動するようにします。

  • TabViewの引数にselectedを指定することで、カスタムタブと連動させる
  • PageTabViewStyleを指定することで、横スワイプでの切り替えを可能にする
ZStack {
    
    // 背景色.
    Color("bg").ignoresSafeArea()
    
    // メイン画面部分はTabViewで定義.
    TabView(selection: $selected) {
        HomeView()
            .tag(TabItem.piyo)
        ListView()
            .tag(TabItem.pen)
        SearchView()
            .tag(TabItem.neko)
        SettingView()
            .tag(TabItem.tobipen)
    }
    // PageTabスタイルを利用する(インジケータは非表示).
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    
    VStack {
        // 省略.
    }
}

まとめ

タブの定義がずいぶん簡単にできる印象ですが、それ以上に「切り替え用のUI」を簡単に作成できるのは嬉しいですね。

こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。 hoshi0523.hatenablog.com

SwiftUI 2.0で matchedGeometryEffect を使ってみる

SwiftUI 2.0で追加されたAPI

SwiftUI 2.0で追加された新機能のうち、Heroアニメーションを簡単に作れるAPIがあったのでちょっと触ってみました。

とりあえず完成形

こんな感じのSegmentControlっぽいUIを作ってみます。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7478/bf3ebbd6-0ab7-922c-1532-cd1b86dc1333.gif

Githubはこちらです。 github.com

開発環境

ボタンを作る

選択に使うボタンのViewを作ります。 そのまえに、適当にenumを定義しておきました。SF Symbolsから、適当に4つほど選出しています。

enum ButtonType: String, CaseIterable {
    case share = "square.and.arrow.up"
    case trash = "trash"
    case folder = "folder"
    case person = "person"
}

ボタンのビューはこんな感じで作ります。
AccentColorについては、適宜Assetsで好きな色を定義してください。

struct CustomButton: View {
    // 選択状態を表すプロパティ.
    @Binding var selected: ButtonType
    // 自分自身のボタンタイプ.
    let type: ButtonType
    
    var body: some View {
        ZStack {
            // 選択中だったら背景に円を描画する.
            if selected == type {
                Circle()
                    // AccentColorはAssetsで定義すること.
                    .fill(Color.accentColor) 
            }
            
            Button(action: {
                // ボタンをタップしたら選択状態を切り替える.
                selected = type
            }, label: {
                // enumから画像を表示する.
                Image(systemName: type.rawValue)
                    .resizable()
                    .renderingMode(.original)
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 44, height: 44)
            })
        }
        .frame(width: 80, height: 80)
    }
}

画面上にボタンを並べる

では、画面上にボタンを並べてみます

struct ContentView: View {
    
    @State private var selected = ButtonType.share // 選択状態の初期値.
    
    var body: some View {
        HStack {
            // enumをforeachで回して、CustomButtonを横に並べる.
            ForEach(ButtonType.allCases, id: \.self) { type in
                CustomButton(selected: $selected, type: type)
            }
        }
    }
}

では動かしてみます。 0002.gif

選択状態を切り替えて、見た目も変わるようになりましたね。では、ここからアニメーションを追加していきましょう。

アニメーションさせる

まずはボタンの選択時の状態変化がアニメーションを伴うように、ボタンタップ時の挙動を一部修正します。

// 一部抜粋.
            
Button(action: {
    // アニメーションを伴わせる
    withAnimation {
        selected = type
    }
}, label: {
    // 省略
})

.matchedGeometryEffectを設定する

アニメーションさせたいViewに対して.matchedGeometryEffectを指定します。 これは、識別子とNamespaceを与えて、同期したいアニメーションをグルーピングする感じです。

まずはnamespaceを宣言します。

struct CustomButton: View {
    // 省略

    var namespace: Namespace.ID // namespaceを追加する.
    
    // 省略
}

次に、アニメーションさせたい背景ビューに対して.matchedGeometryEffectを指定します。識別子は、アニメーションを同期したいグループ間で一致していれば何でもいいです。

// 選択中だったら背景に円を描画する.
if selected == type {
    Circle()
        .fill(Color.accentColor)
        .matchedGeometryEffect(id: "CustomButton", in: namespace)
}

次に、呼び出しているView側に修正を加えます

struct ContentView: View {
    
    @State private var selected = ButtonType.share

    // 追加する.
    @Namespace var namespace

    var body: some View {
        HStack {
            ForEach(ButtonType.allCases, id: \.self) { type in
                // 引数にnamespaceを与えるように修正する.
                CustomButton(
                    selected: $selected,
                    type: type,
                    namespace: namespace
                )
            }
        }
    }
}

以上で完成です! とても簡単にできちゃいますね!

まとめ

Heroアニメーションはテンション上がるので、他にも色々と試して記事にしたいと思います!

こちらの書籍はとても参考になったのでおすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI

やれたかも委員会 をリリースしました!

新作アプリを公開しました

f:id:hoshi0523:20180503135625p:plain:w300

みなさま、いつもご利用頂きありがとうございます。
Susumu Hoshikawaでございます。

今回は、新作マンガアプリ やれたかも委員会 のリリースご報告です!

やれたかも委員会

やれたかも委員会

  • Susumu Hoshikawa
  • ブック
  • 無料

ドラマでも絶賛放送中!

yaretakamoiinkai.com

現在、TBSにて、ドラマでも絶賛放送中の やれたかも委員会 を、この度マンガアプリとしてリリースさせていただきました!

やれたかも委員会って?

すでにTVでもご覧になった方もいらっしゃると思いますが、吉田貴司さん原作のマンガで、現在はnoteやマンガ on ウェブなどで連載中の作品です。

毎回、女性との「やれたかもしれない」(セックスできたかもしれない)思い出を持つ男性が、面接会場のような場所を訪れ、女性と甘酸っぱさやほろ苦さ残る思い出を独白形式で語る。聞き手である男性2名、女性1名で構成される「やれたかも委員会」メンバーが、最後に「やれた」か「やれたとは言えない」かを判定する。(wikipediaより抜粋)

第1巻は完全に無料で!

現在は2巻まで刊行中ですが、第1巻は完全無料となっております。第2巻の価格は¥240となっております。

ぜひこの機会にお試しください!

やれたかも委員会

やれたかも委員会

  • Susumu Hoshikawa
  • ブック
  • 無料

【ついに完結】特攻の島 最終巻 第9巻を公開しました

みなさま、いつも当アプリをご利用頂きまして、ありがとうございます。Susumu Hoshikawaでございます。

今回は、拙作アプリ無料で1巻!特攻の島に関するご報告です。

特攻の島

特攻の島

  • Susumu Hoshikawa
  • ブック
  • 無料

特攻の島 最終巻 ついに完結

佐藤秀峰さんが手がける漫画特攻の島が、長年の連載を経て、ついに完結いたしました。

そして、その最終巻である第9巻が、iPhoneiPadアプリ特攻の島にて、本日より配信を開始いたしました!

漫画「特攻の島」は、特攻兵機「回天」の搭乗員の姿を描いた物語で、当然舞台は太平洋戦争真っ只中。最後の作戦に挑む渡辺の運命は!?是非ともご覧下さい!

最終巻の表紙はこんな感じ!

最終巻の表紙はこのようになっています。

第1巻では、表情に幼さも残していた渡邊でしたが、物語をしめくくるこの最終巻で、何かを悟るような表情を見せています。どのような形で物語が完結するのかは、ぜひ皆様自身の目でお確かめください。

f:id:hoshi0523:20180221221906j:plain:w300


価格は¥240!

第2〜8巻と同様、価格は¥240となっております。書籍版と比べると、なんと半額以下です!

場所もとらずに安くあがる、いつでもどこでも読めるiPhone/iPadアプリ版の特攻の島、どうぞお楽しみください!

特攻の島

特攻の島

  • Susumu Hoshikawa
  • ブック
  • 無料

以上、今後とも当アプリをよろしくお願いいたします!

「ブラックジャックによろしく」の有料化に関するご連絡

f:id:hoshi0523:20140507192802p:plain

この度、2017年08月31日付で、電子書籍ブラックジャックによろしく」の取り扱いを有料とさせていただきました事をご連絡いたします。

価格は、全13巻セットで ¥120 となっております。

これまでの二次利用規約

漫画「ブラックジャックによろしく」は、2012年09月12日に二次利用のフリー化が行われ、誰でも規約に則り、自由に作品を利用することが可能となりました。

当アプリでも、この規約に則り、無料で作品を配布させていただいておりました(新ブラックジャックによろしくは無料配布の対象外です)。

有料化について

この度、作者様の意向により、この二次利用規約に改定が行われました。

改定された規約では、アプリでは無料での取り扱いができないものとなっております。

以下、改定後の規約の抜粋です。

5.「ブラックジャックによろしく」作品を電子書籍配信ストアにて配信する場合は、必ず 1円以上の有償販売としてください。なお、その場合に生じた利益については、弊社及び 佐藤秀峰に分配する必要はありません。

このため、本日 2017.08.31 に公開しましたバージョン(ver 6.0.0)からは、ブラックジャックによろしくは有料とさせていただいております。

旧バージョンのアプリについて

これまでのバージョンのアプリでは、ブラックジャックによろしくは閲覧できないようになっております。購読される場合は、新しいバージョンに更新していただいた上で、ご購入いただきますよう、よろしくお願いいたします。

無料での配布について

なお、作者様が運営されているサイトでは、引き続き無料での配布が行われております。当アプリでは無料での配布はできなくなりましたが、こちらをご利用いただくことで、引き続き無料での閲覧が可能となっておりますので、合わせてご利用いただければと思います。

Web漫画 Webコミック 無料で漫画読み放題 マンガonウェブ

以上です。 大変申し訳ございませんが、ご理解いただけますよう、よろしくお願いいたします。