20200212のiOSに関する記事は7件です。

Swift Stateとはなんぞや1コマ劇場

はじめに

自分のSwift学習メモです。
1枚のメモ用紙に、その宣言や使い方のざっくりしたイメージを記録していきます。

対象とする読者

自分
SwiftUI初心者

環境

私がこの記事を書いている際に利用しているのは次の環境です。
Xcode 11.3.1
Swift 5.0

@Stateを1枚で説明すると

@State.png

解説

変数に@Stateをつけることで、State構造体という特別な場所に変数は格納されます。そして、この変数の値が書き換わるたびに、State構造体に定義されているupdate()が呼び出され、関連する各Viewの再表示が行われます。
状態変更による個別Viewの再表示イベントの実装から解放される模様。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

作りながら学ぶ! SwiftUIアニメーション インジケーター編

はじめに

作りながら学ぶ! アニメーション インジケーター編です。

完成するとこのようなアニメーションになります。
ylcjk-49v22.gif

作ろう

円を作る

まずは土台となる円を作ります。

import SwiftUI

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

スクリーンショット 2020-02-04 13.28.00.png

円をくり抜いて輪を作る

円を輪にするにはstrokeメソッドを使います。

func stroke<S>(S, lineWidth: CGFloat) -> View
func stroke<S>(S, style: StrokeStyle) -> View
func stroke(lineWidth: CGFloat) -> Shape
func stroke(style: StrokeStyle) -> Shape

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

ここの<S>にはShapeStyleに準拠している型を指定することができます。

ColorやGradientといった色や色のグラデーションを指定することができます。
https://developer.apple.com/documentation/swiftui/shapestyle

最初はわかりやすいように色をグリーンにします。

struct ContentView: View {
    var body: some View {
            Circle()
                .stroke(Color.green)
    }
}

スクリーンショット 2020-02-09 23.52.34.png

これでは線が細くて見づらいので、線の幅を広げて見やすいようにしましょう。
そのためにStrokeStyleを当てていきます。

StrokeStyle

境界線または仕切りの色、幅、およびスタイルを定義するオブジェクト。
https://developer.apple.com/documentation/apple_news/strokestyle

        StrokeStyle(lineWidth: CGFloat,
                    lineCap: CGLineCap,
                    lineJoin: CGLineJoin,
                    miterLimit: CGFloat,
                    dash: [CGFloat],
                    dashPhase: CGFloat)

lineWidth

線の幅 デフォルトは1

ですので線幅を大きくするために、lineWidthを8にします。

struct ContentView: View {
    var body: some View {
            Circle()
                .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
    }
}

スクリーンショット 2020-02-10 0.24.06.png

円のままだと、回転してもわからないのでtrimメソッドを使ってトリミングしていきます。

trim(from:to:)

func trim(from startFraction: CGFloat = 0, to endFraction: CGFloat = 1) -> some Shape

https://developer.apple.com/documentation/swiftui/shape/3365372-trim

struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.4)
            .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
    }
}

スクリーンショット 2020-02-11 15.47.29.png

現在のままだと円が大きいので、frameメソッドを使い調整します。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green, style: StrokeStyle(lineWidth: 8))
            .frame(width: 48, height: 48)
    }
}

スクリーンショット 2020-02-11 15.54.22.png

インジケーターっぽいサイズになってきました!

スクリーンショット 2020-02-10 0.59.15.png

角が尖っているのを、丸くしていきます。
そのためにStrokeStylelineCapを利用します。

CGLineCap (iOS 2.0+)

CGLineCap 線の端 末端 画像
.butt 四角 指定されたパスのエンドポイントまで スクリーンショット 2020-02-08 22.37.32.png
.round 指定されたパスの端点を超える スクリーンショット 2020-02-08 22.37.42.png
.square 四角 指定されたパスの端点から線幅の半分だけ超える スクリーンショット 2020-02-08 22.37.52.png

https://developer.apple.com/documentation/coregraphics/cglinecap

struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(lineWidth: 8, lineCap: .round))
            .frame(width: 48, height: 48)
    }
}

スクリーンショット 2020-02-10 1.29.45.png

CGLineJoin (iOS 2.0+)

以下のように角がある図形だと変化がわかります。
しかし、今回作成するインジケーターは円弧なので軽く流します。

.miter .round .bevel
lineJoin スクリーンショット 2020-02-09 0.25.20.png スクリーンショット 2020-02-09 0.25.27.png スクリーンショット 2020-02-09 0.29.17.png

miterLimit

先端部の形状に.miterを適用を決める閾値(デフォルト 10)

dash

破線の形状を配列で指定します。

[線の長さ, 空白の長さ, 2番目の線の長さ, 2番目の空白の長さ, ...]
struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.4)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            .frame(width: 48, height: 48)
    }
}

スクリーンショット 2020-02-10 2.45.53.png

dashPhase

破線の開始位置を変更します。デフォルトは0

回転させよう

いよいよアニメーションです。
2次元の回転系のアニメーションには、rotatioinEffectを使います。

rotationEffect(_:anchor:)

func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View

https://developer.apple.com/documentation/swiftui/text/3276966-rotationeffect
angleには回転する角度を指定します。
anchorには回転する中心を指定します。デフォルトは.centerです。

Angle

https://developer.apple.com/documentation/swiftui/angle
Angleにはdegreesradiansを渡すことができます。
今回は角度を渡すことにするのでdegreesを引数にとります。

struct ContentView: View {
    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))
            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: 180))
    }
}

真ん中を中心に180度回転しました。
スクリーンショット 2020-02-10 3.16.44.png

しかしこれでは、アニメーションとは言えません。
ですので、時間経過とともに変化するようにしていきます。

onAppear

まずはじめに、どのタイミングでアニメーションが発火するか決めます。
ボタンを押したら、スクロールしたら、など色々ありますが、今回は対象となるViewが表示されたアニメーションが発火するようにします。
そこでonAppearメソッドを利用します。

func onAppear(perform action: (() -> Void)? = nil) -> some View

このメソッドを呼ぶことでViewが用事されたときにactionクロージャを実行します。

https://developer.apple.com/documentation/swiftui/text/3276931-onappear

次に肝心のアニメーションの処理です。
今回は withAnimationメソッドを利用します。

withAnimation

指定したアニメーションとともにViewを更新します。
https://developer.apple.com/documentation/swiftui/3279151-withanimation
bodyにはアニメーションとともに変化させたい状態変数をクロージャに渡します。

Animation

Animatioin 説明 gif
default デフォルト default.gif
linear 直線的 一定の割合 lineaer.gif
easeIn 徐々に早くなる easeIn.gif
easeOut 徐々に遅くなる easeOut.gif
easeInOut 開始は遅く、中盤で加速し終盤でまた遅くなる easeInOut.gif

今回は一定速度で回り続けて欲しいのでlinearを利用します。

lienarメソッドにはアニメーション長さを指定することができます。
lienar(duration:)を利用します。

@State

状態変数をセットします。
これをwithAnimationのクロージャ内で切り替えてあげることで状態が変化します。

struct ContentView: View {

    @State var isAnimation = false

    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))

            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
            .onAppear() {
                withAnimation(
                    Animation
                        .linear(duration: 1)) {
                            self.isAnimation.toggle()
                }
        }
    }
}

このままですと、一度回転しただけで終わってしまいますので、repeatForever(autoreverses:)メソッドを利用します。

repeatForever(autoreverses:)

https://developer.apple.com/documentation/swiftui/animation/3263783-repeatforever

autoreverses
true true.gif
false false.gif

リバースしなくて良いので、falseを指定します。

struct ContentView: View {

    @State var isAnimation = false

    var body: some View {
        Circle()
            .trim(from: 0, to: 0.6)
            .stroke(Color.green,
                    style: StrokeStyle(
                        lineWidth: 8,
                        lineCap: .round,
                        dash: [0.1, 16]))

            .frame(width: 48, height: 48)
            .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
            .onAppear() {
                withAnimation(
                    Animation
                        .linear(duration: 1)
                        .repeatForever(autoreverses: true)) {
                            self.isAnimation.toggle()
                }
        }
    }
}

RoundedRectangle

グラデーション

Gradient(colors: [.gray, .white])
グラデーション 見た目
Linear 線形 スクリーンショット 2020-02-11 11.37.07.png
Angular 円形 スクリーンショット 2020-02-11 11.15.46.png
Radical 放物状 スクリーンショット 2020-02-11 11.37.49.png

.strokeメソッドの中のColor.greenAngularGradient(gradient: Gradient(colors: [.gray, .white])に変更します。
そうすると以下のように破線の位置がずれていることが確認できます。

スクリーンショット 2020-02-11 2.06.21.png

そこでStrokeStyledashPhaseの値をかえます。

dashPhase: 8にすることで、

スクリーンショット 2020-02-11 2.07.40.png

綺麗に描画されました。

 完成済みソースコード

import SwiftUI

struct ContentView: View {

    @State var isAnimation = false

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .frame(width: 200, height: 120, alignment: .center)
                .foregroundColor(Color.gray)

            VStack {
                Spacer()
                Circle()
                    .trim(from: 0, to: 0.6)
                    .stroke(AngularGradient(gradient: Gradient(colors: [.gray, .white]), center: .center),
                            style: StrokeStyle(
                                lineWidth: 8,
                                lineCap: .round,
                                dash: [0.1, 16],
                                dashPhase: 8))

                    .frame(width: 48, height: 48)
                    .rotationEffect(Angle(degrees: self.isAnimation ? 360 : 0))
                    .onAppear() {
                        withAnimation(
                            Animation
                                .linear(duration: 1)
                                .repeatForever(autoreverses: false)) {
                                    self.isAnimation.toggle()
                        }
                }

                Text("読み込み中")
                    .foregroundColor(.white)
                    .font(.system(size: 12, weight: .medium, design: .rounded))
                    .lineLimit(1)
                    .padding(.top)
                Spacer()
            }
        }
    }
}

課題

strokestykeのdashにアニメーション当てれなかった;;
dashの幅を変えて、アニメーションをeaseOutでアニメーションの終わりに一つ一つの玉が近くアニメーションを作りたかった;;
誰か教えてくださいmm

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初心者がSwiftでマルバツゲームのiOSアプリ作ってみた

はじめに

研究室の同期たちとの第三回ハッカソンが開催されたのでその記録。
他のメンバーの記事はこちら。
指定したユーザーのツイートをしゃべるRaspberry Piの作り方
位置情報を使ってSlackのステータスを更新してみた
はじめはtemiロボット用のアプリを作ろうと思ったが、色々うまくいかず断念...
そこで、今回は研究用にiOSアプリを作るためなんとなくSwiftを勉強した記憶を思い出しつつ何か作ってみる。
今回は、夏に作りかけていた簡単なマルバツゲーム:o::heavy_multiplication_x:を完成させて、追加要素としてマルバツゲームの盤面のベクトルを変更できるようにしてみる。

以下のサイトが参考になった。
Swiftで初めてiPhoneゲーム開発する人向チュートリアル(マルバツゲームを例にStep by Step解説)

準備編

準備したもの

MacBook Air
iPad Pro
Xcode 10.3
Swift 5.0
マルバツゲームの盤面の画像
マル・バツの画像

プロジェクトの作成

Xcodeを起動して、「Single View App」でプロジェクト作成。
保存場所とかは好きなところで、プロジェクト名は適当に「MyGame」にした。
スクリーンショット 2020-02-12 17.34.53.png

アプリ画面の準備

1.画面左上の「ProjectNavigater」からMain.storyboardファイルを選択。
スクリーンショット 2020-02-12 17.39.46.png

2.画面右上の「Show the Object Library」から、「Button」や「Text Field」、「Image View」など必要なものをstoryboard画面にドラッグ&ドロップで配置。

3.画面右上のアイコン「Show the Assistant editor」をクリックして、storyboard画面の隣に「ViewController.swift」を表示。
スクリーンショット 2020-02-12 17.48.38.png

4.それぞれ配置したオブジェクトをクリックして、青くなった状態でCtrlキーを押しながらViewControllerへドラッグ&ドロップする。出てきたポップアップ上でコネクション情報を設定。
スクリーンショット 2020-02-12 17.57.22.png

5.nameをそれぞれに対応したものに設定を完了。以下の例はターンを表示するラベルのText Field。

ViewController.swift
@IBOutlet weak var turnLabel: UILabel!

コネクションをそれぞれのオブジェクトで設定して画面は完成。
実装編でそれぞれの機能を作っていく。
いるものを全部配置したらこんな感じ。
スクリーンショット 2020-02-12 17.25.24.png

実装編

マルバツゲーム:o::heavy_multiplication_x:を作ってみる

まずはシンプルなマルバツゲームを作成する。
全体を通して思いつきでやってる部分もあるのでかなり雑なコードになった...

各マスの座標を設定したり、マル・バツを置くたびにターンを変更したりする。
さらに、それぞれのマスのマル・バツを更新する関数を作る。

ViewController.swift
    @IBOutlet weak var turnLabel: UILabel!
    var turn: Int = 0

    //座標とかを設定
    var status: [Int] = [0,0,0,0,0,0,0,0,0]
    var squares: [[Int]] = [[-1,1,0],[0,1,0],[1,1,0],
                            [-1,0,0],[0,0,0],[1,0,0],
                            [-1,-1,0],[0,-1,0],[1,-1,0]]
    let winMaru: [Int] = [1,1,1]
    let winBatsu: [Int] = [-1,-1,-1]


    //プレイヤーのターンを管理
    @IBAction func tap(_ sender: UIButton) {
        let button = (sender as UIButton)

        if (turn % 2) == 0 {
            button.setImage(UIImage(named: "mark_maru"), for: UIControl.State())
            turnLabel.text =  "X の番です"
            status[sender.tag - 1] = 1
        }
        else{
            button.setImage(UIImage(named: "mark_batsu"), for: UIControl.State())
            turnLabel.text =  "◯ の番です"
            status[sender.tag - 1] = -1
        }
        check(status)
        turn += 1
    }

    func update(_ statusArray: [Int], _ squaresArray: inout [[Int]]){
        for i in 0...8{
            squaresArray[i][2] = statusArray[i]
        }
    }

   //中略

    //各マスの情報を更新
    func updateImage(_ squaresArray: [Int]){
        if squaresArray[0] == 1{
            button0.setImage(UIImage(named: "mark_maru"), for: UIControl.State())//まるにする
        }
        else if squaresArray[0] == -1{
            button0.setImage(UIImage(named: "mark_batsu"), for: UIControl.State())//ばつにする
        }
        else{
            button0.setImage(UIImage(named: "white"), for: UIControl.State())//しろにする
        }

        if squaresArray[1] == 1{
            button1.setImage(UIImage(named: "mark_maru"), for: UIControl.State())//まるにする
        }
        else if squaresArray[1] == -1{
            button1.setImage(UIImage(named: "mark_batsu"), for: UIControl.State())//ばつにする
        }
        else{
            button1.setImage(UIImage(named: "white"), for: UIControl.State())//しろにする
        }

        if squaresArray[2] == 1{
            button2.setImage(UIImage(named: "mark_maru"), for: UIControl.State())//まるにする
        }
        else if squaresArray[2] == -1{
            button2.setImage(UIImage(named: "mark_batsu"), for: UIControl.State())//ばつにする
        }
        else{
            button2.setImage(UIImage(named: "white"), for: UIControl.State())//しろにする
        }

//中略

        if squaresArray[8] == 1{
            button8.setImage(UIImage(named: "mark_maru"), for: UIControl.State())//まるにする
        }
        else if squaresArray[8] == -1{
            button8.setImage(UIImage(named: "mark_batsu"), for: UIControl.State())//ばつにする
        }
        else{
            button8.setImage(UIImage(named: "white"), for: UIControl.State())//しろにする
        }
    }

次に勝利条件をチェックする関数も作る。

ViewController.swift
    func check(_ squaresArray: [Int]){
        print(squaresArray)
        for i in 0...2{
            if (squaresArray[3 * i] == 1) && (squaresArray[(3 * i) + 1] == 1) && (squaresArray[(3 * i) + 2] == 1){
                turnLabel.text = "◯ の勝ち!!"
                break
            }
            if (squaresArray[3 * i] == -1) && (squaresArray[(3 * i) + 1] == -1) && (squaresArray[(3 * i) + 2] == -1){
                turnLabel.text = "X の勝ち!!"
                break
            }
            if (squaresArray[0 + i] == 1) && (squaresArray[3 + i] == 1) && (squaresArray[6 + i] == 1){
                turnLabel.text = "◯ の勝ち!!"
                break
            }
            if (squaresArray[0 + i] == -1) && (squaresArray[3 + i] == -1) && (squaresArray[6 + i] == -1){
                turnLabel.text = "X の勝ち!!"
                break
            }
            if (squaresArray[0] == 1) && (squaresArray[4] == 1) && (squaresArray[8] == 1){
                turnLabel.text = "◯ の勝ち!!"
                break
            }
            if (squaresArray[0] == -1) && (squaresArray[4] == -1) && (squaresArray[8] == -1){
                turnLabel.text = "X の勝ち!!"
                break
            }
            if (squaresArray[2] == 1) && (squaresArray[4] == 1) && (squaresArray[6] == 1){
                turnLabel.text = "◯ の勝ち!!"
                break
            }
            if (squaresArray[2] == -1) && (squaresArray[4] == -1) && (squaresArray[6] == -1){
                turnLabel.text = "X の勝ち!!"
                break
            }
        }
    }

とりあえずここまではこんな感じ。
スクリーンショット 2020-02-12 18.28.28.png
進めていくと
スクリーンショット 2020-02-12 18.28.49.png
こんな感じで勝ちのプレイヤーが表示される。

マルバツゲームの盤面のベクトルを変えれるようにする

上下左右に対応したボタンを押すと、その方向にベクトルがかかって一度置いたマル・バツがずれるようにしたい。
なんか重力操れるみたいでかっこいいよね。

「up」ボタンを押したら上方向のベクトルがかかるようにする関数を作成。

ViewController.swift
//上方向のベクトル
func gravityU(_ squaresArray: inout [Int]){
        var line1: [Int] = [squaresArray[0],squaresArray[3],squaresArray[6]]
        var line2: [Int] = [squaresArray[1],squaresArray[4],squaresArray[7]]
        var line3: [Int] = [squaresArray[2],squaresArray[5],squaresArray[8]]
        var lineArray: [Int] = []
        /*-------------------------------*/
        for i in line1{
            if i != 0{
                lineArray.append(i)
            }
            line1 = lineArray
        }

        let line1Count = line1.count
        if line1Count == 0{
            line1 = [0,0,0]
        }
        else if line1Count == 1{
            line1.append(0)
            line1.append(0)
        }
        else if line1Count == 2{
            line1.append(0)
        }
        squaresArray[0] = line1[0]
        squaresArray[3] = line1[1]
        squaresArray[6] = line1[2]
        lineArray = []
        /*-------------------------------*/
        for i in line2{
            if i != 0{
                lineArray.append(i)
            }
            line2 = lineArray
        }
        let line2Count = line2.count
        if line2Count == 0{
            line2 = [0,0,0]
        }
        else if line2Count == 1{
            line2.append(0)
            line2.append(0)
        }
        else if line2Count == 2{
            line2.append(0)
        }
        squaresArray[1] = line2[0]
        squaresArray[4] = line2[1]
        squaresArray[7] = line2[2]
        lineArray = []
        /*-------------------------------*/
        for i in line3{
            if i != 0{
                lineArray.append(i)
            }
            line3 = lineArray
        }
        let line3Count = line3.count
        if line3Count == 0{
            line3 = [0,0,0]
        }
        else if line3Count == 1{
            line3.append(0)
            line3.append(0)
        }
        else if line3Count == 2{
            line3.append(0)
        }
        squaresArray[2] = line3[0]
        squaresArray[5] = line3[1]
        squaresArray[8] = line3[2]
        lineArray = []
        /*-------------------------------*/
    }

上下左右、他の方向分の関数も作成。

完成

とりあえず、完成。
動かしてみるとこんな感じ。
スクリーンショット 2020-02-12 17.25.45.png
ここで「up」ボタンを押すと
スクリーンショット 2020-02-12 17.25.54.png
こんな風に上にマル・バツがずれる。:arrow_double_up:
勝利条件のチェックも問題なくできるので、とりあえずこれでok。

終わりに

久しぶりにSwift触ってみたけど、自分のアイデアとかが目に見える形で実際に動くのは面白い。
まだUIとかボタンの制約とかが荒削りなので、今後これをもっと作り込みたい。

以上、第三回研究室ハッカソン備忘録。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]カスタムドキュメントのサムネイルを表示する - Thumbnail Extension

こんにちは。
@gachalatteです。

今回は、カスタムドキュメントのサムネイルを表示するThumbnail Extensionを紹介します。Thumbnail Extensionの実装は難しいものではありませんが、インターネットで検索してもほとんど情報が得られない状況ですので、ここにまとめておきます。

前回の記事とあわせてご覧ください。

プロジェクトにThumbnail Extensionを追加する

  1. Xcodeのメニューから、File > New > Target...を選択します。
  2. Application Extension > Thumbnail Extensionを選択します。
  3. Product NameThumbnailExtension(任意)を入力します。

プロジェクトを追加

Info.plistにカスタムドキュメントを登録する

  1. DocumentBaseApp > Targets > ThumbnailExtension > Infoを開きます。
  2. NSExtension > NSExtensionAttributes > QLSupportedContentTypesにカスタムドキュメントのUTIを追加します。

Info.plist

ThumbnailProviderを実装する

ThumbnailExtension > ThumbnailProvider.swiftを開いて実装します。ThumbnailProviderの実装方法が3種類あることが示されていますので、最適な方法を選択してください。

ThumbnailProvider.swift
class ThumbnailProvider: QLThumbnailProvider {

    override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {

        // There are three ways to provide a thumbnail through a QLThumbnailReply. Only one of them should be used.

        // First way: Draw the thumbnail into the current context, set up with UIKit's coordinate system.
        handler(QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
            // Draw the thumbnail here.

            // Return true if the thumbnail was successfully drawn inside this block.
            return true
        }), nil)

        /*

        // Second way: Draw the thumbnail into a context passed to your block, set up with Core Graphics's coordinate system.
        handler(QLThumbnailReply(contextSize: request.maximumSize, drawing: { (context) -> Bool in
            // Draw the thumbnail here.

            // Return true if the thumbnail was successfully drawn inside this block.
            return true
        }), nil)

        // Third way: Set an image file URL.
        handler(QLThumbnailReply(imageFileURL: Bundle.main.url(forResource: "fileThumbnail", withExtension: "jpg")!), nil)

        */
    }
}

今回は、ドキュメント内のimage.pngをそのまま返すように実装してみます。(実際は、QLFileThumbnailRequestの要求に従った画像を返すようにしてください。)

ThumbnailProvider.swift
class ThumbnailProvider: QLThumbnailProvider {

    override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
        let imageFileURL = request.fileURL.appendingPathComponent("image.png")
        handler(QLThumbnailReply(imageFileURL: imageFileURL), nil)
    }
}

デバッグする

ターゲットをThumbnailExtensionにして実行するとデバッグすることができます。起動するAppを尋ねられたらDocumentBaseAppを選択します。この挙動は、Product > Scheme > Edit Scheme... > Run > Info > Executableで変更することができます。

完成

ThumbnailExtensionの実装が完成したら、ターゲットをDocumentBasedAppに戻します。

UIDocumentBrowserViewControllerにサムネイルが表示されるようになりました。ファイル.appでもサムネイルが表示されます。

スクリーンショット

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Can’t end BackgroundTask:のエラーについて

Can’t end BackgroundTask: no background task exists with identifier 1 (0x1), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.


キーワード
UIApplicationEndBackgroundTaskError

iOSのログに出ていたエラーについて
このエラーが最近特に気になるのですが、何が原因で起きてるのかわからなかったので調べてみました。

XcodeのバージョンはXcode11
OSはiOS13の実機、シミュレーションです。

アプリをバックグラウンドに移行させた時にエラーが出る。

調べた結果
Apple開発者フォーラムのによると
https://forums.developer.apple.com/thread/121990

iOS 13.0で「BackgroundTaskを終了できません」エラーメッセージ

BackgroundTaskを終了できません:識別子1(0x1)のバックグラウンドタスクが存在しないか、既に終了している
可能性があります。UIApplicationEndBackgroundTaskError()を中断してデバッグします。


シーンを使用していません。古いアプリのデリゲートは、使用しないことを選択した場合でもiOS13
に適切ですか?5年前のiOSアプリがあるため、Scenesを使用しなくても正常に動作しますが、バックグラウンド
タスクの警告が表示されます


こんにちは、Firebase Cloud Notificationも使用していますか?
私は同じ問題を抱えています。
注目してくれてありがとう。


Firebase Cloud Notificationを使用しません。
この問題は、新しいプロジェクトを実行しても発生します。


iOS 13-beta8で同じバックグラウンドタスクの問題が発生し、AVCaptureSessionを管理しています。
AVCaptureSessionがAVCaptureConnectionでフレームの配信を開始すると、バックグラウンドタスクを
開始します。AVCaptureSessionが中断された場合は、AVCaptureVideoDataOutputへの接続を介して
すべてのフレームがフラッシュされるようにしてパイプラインを破棄し、バックグラウンドタスクを終了します。
iOS 13では有効期限ハンドラーが起動していないこと、およびタスクを完了すると常にfrnkwkが最初に
投稿したのと同じエラーメッセージが発生することを確認できます。


報告者

iPhone XS MaxのXcode 11 GMとiPhone XSのiOS 13.1 Beta 3のiOS 13 GMの空の新しいプロジェクトでも同じです。

BackgroundTaskを終了できません。識別子2(0x2)のバックグラウンドタスクが存在しないか、既に終了している可能性があります。UIApplicationEndBackgroundTaskError()を中断してデバッグします。
これはiOS 12.4では発生していません。iOS 13では、フォアグラウンドに戻ったときにビデオセッションを再開できません。
iOS13.2.2で修正されたとか本番コードに誤って残されたデバッグステートメントじゃないかとかいろいろ議論してますが要約すると、iOS13.2.2でも修正はされていません。

実害が今のところないのと、Appleに報告済みのようなので見て見ぬ振りをするしかないですね、、


報告者

古いライフサイクルメソッドをまだ使用している場合、これは無害な警告ですが、スイッチをオフにできると便利です。
アプリはクラッシュすることなく前景と背景をうまく切り替えます。そして、バックグラウンドに入るたびにこのメッセージを受け取ります。


報告者

iOS 13.0のXcode 11.0で同じ問題が発生しています。ホームボタンを押すか、バックグラウンドフェッチを
シミュレートすると、シミュレータとiPhoneの両方でエラーが発生します。また、これを新しいシングルビューアプリ
プロジェクトでテストし、ホームボタンを押すと同じエラーが発生することを確認しました。


報告者

アプリを切り替えると、新しい単一のアプリプロジェクトで同じ問題が発生します。
BackgroundTaskを終了できません:識別子19(0x13)のバックグラウンドタスクが存在しないか、既に終了している可能性があります。UIApplicationEndBackgroundTaskError()を中断してデバッグします。


報告者

13.2で解決されたと言っている人を見ましたが、それは私の経験ではありませんでした。しかし、
問題があります-このエラーの後、アプリはときどきクラッシュします。

13にあげた時のバグの場合がある?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Storyboardとsegueを利用しないデータをパスする方法~~ Pass data without segue in Swift

本記事はSwiftを勉強する時個人的なメモとします.役に立てればうれしいです.:grinning:
本に書いてあるデータをパスする方法の多くは Storyboard 上から線を引いて,"Identifier" に名前をつけてから segue によりデータをパスする方法となっています.

UserDefaultsを使用たデータパス

UserDefaults はローカルにある軽いデータを記憶するのによく使われる.例えばユーザーが入力したIDやパスワードを記憶するため使うのが便利である.手軽にデータの永続化ができるため、多くの場面で利用される機能です.

対応できる型はInt,Float,Double,BOOL,Array,Dictionary,そしてAnyも対応可能です.

:bangbang:注意:UserDefaults を利用して大量なデータを保存することはおすすめしません.
UserDefaults に保存されたデータは .plist ファイルの中に格納するため,理論上 UserDefaults に数GBのデータを保存することができるが,アプリを起動するたびに, UserDefaults 内のすべてのデータをロードするため,ファイルが大きくなると起動が遅くなり,アプリの性能に影響を与える.
そのため,大量なデータを保存するとき Core Data、 CloudKit、SQLite を利用するといいです.

実装方法

// --ファイルA--
var i_want_pass_this_data:Int = 1 + 1
// データをパスする関数を作る.
func GoPass() {
    UserDefaults().setValue(i_want_pass_this_data, forKey: "PleasePassMyData")
}

override func viewDidLoad() {
   super.viewDidLoad()
   // 関数を実行するとデータがパスされる.
   GoPass()
}

// --ファイルB--
// データを受け取る.
let getDATA = UserDefaults().string(forKey: "PleasePassMyData") ?? ""
// getDATAは2となる.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]ビリビリ動画にあるようなリフレッシュする際にGIFアニメーションを見せるビュー

はじめに

TableViewを引っ張ってリロードすることをリフレッシュと呼びますが、
リフレッシュする際にアニメーションを見せることにより、ユーザが待ち時間に感じる退屈を防ぐことができます。
実際に中国大手の動画共有サイト「bilibli」のスマホアプリでも取り入れられています。
(筆者はbilibiliの世界観を創り出すUIをとても気に入っており、UIに困ると再現して取り込んでしまっています。)

この記事ではリフレッシュする際にGIFアニメーションを見せるTableViewの作り方を共有したいと思います。

目次

  • 環境
  • 実行例
  • 考え方
    • 画面の構成
    • ロード時に画面を固定して見せる
      • GIFアニメーションを表示する
      • UIImageViewをロード時に固定表示し、処理後に初期状態に戻す
  • ソースコード
  • おわりに

環境

SwiftGifOriginはCocoaPodsから簡単に入れられます。

実行例

refreshanime.gif

考え方

とてもシンプルに実装することができます。
考えることは、画面の構成ロード時の画面表示についてだけです。
(と言っても細かい工夫は必要になってきます。)

1.画面構成

画面構成(初期状態)は以下のようになっています。

Untitled Diagram.png

TableViewがあって、その上にUIViewが被さっていることがわかります。
さらにUIViewにはheaderUIImageViewが被さっていることがわかりますね。

このUIImageViewGIFアニメーションを表示する領域となります。

2.ロード時の画面表示

ロード時の画面表示でやるべきことは主に2つあります。
GIFアニメーションを表示することと、
UIImageViewをロード時に固定表示し、処理後に初期状態へ戻すことです。

GIFアニメーションを表示する

GIFアニメーションの表示にはSwiftGifOriginという便利なライブラリを利用します。

また、GIFアニメーションはロード時に動かすことが望ましいですよね。
なのでViewの生成時には画像を表示し、ロード時になった場合にGIFを表示するようなライフサイクルにしていこうと思います。
(このテクニックはスプラッシュ画面の作成時などにも利用されます)

以上のことを実現するために必要な関数が3つあります。

実行順番 関数名 処理タイミング 処理内容
1 createHeaderView viewDidLoad時 ビューを作成する
2 addHeaderViewGif リフレッシュ時の最初 ビューにGIFを追加する
3 updateHeaderView リフレッシュ時の最後 ビューをアップデートする

では、それぞれの関数について説明していきます。

createHeaderViewは以下のように表現されます。

private func createHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))

        myLabel.text = "↑"
        myLabel.textAlignment = .center
        myLabel.textColor = .white
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))

        image.image = UIImage(named: "bili")

        myHeaderView.addSubview(image)
    }

ヘッダービューを作成して、そこにUILabelUIImageViewを貼り付けているだけですね。


addHeaderViewGifは以下のように表現されます。

func addHeaderViewGif() {
        let displayWidth: CGFloat! = self.view.frame.width
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))

        image.loadGif(name: "bili")

        myHeaderView.addSubview(image)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.myHeaderView.subviews[1].removeFromSuperview()
        }
    }

ここでGIFアニメーションを追加していますね。

処理の内容としては、

image.loadGif(name: "bilibili")でGIFをUIImageViewにセットして、
静止画像の上に重ねるようにヘッダビューに貼り付けています。

そして、その0.1秒後に静止画像がセットされているUIImageViewを削除しています。

ここで重要なのは処理の順番です。

もしこの処理の流れを逆にしてしまえば、UIImageViewが一瞬存在しない時間が生まれてしまいます。
そのせいで、一瞬消えて再表示されるように見えてしまうんですね。
これでは切り替えているのがバレバレで、自然にアニメーションが動くように感じられません。

それを避けるために、静止画とGIFアニメーションが0.1秒重なるように表示します。


updateHeaderViewは以下のよう表現されます。

private func updateHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        for sub in myHeaderView.subviews {
            sub.removeFromSuperview()
        }
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))

        myLabel.text = "↑"

        myLabel.textAlignment = .center
        myLabel.textColor = .white
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))

        image.image = UIImage(named: "bili")

        myHeaderView.addSubview(image)
    }

createHeaderViewとほとんど同じ処理です。

違うのはビューを重ねないように全て削除してからaddSubView()しているところですね。


以上で関数の説明は終わりです。

リフレッシュに関数する説明も少ししておきます。


リフレッシュ時に呼び出される関数は以下のように定義されます。

  @objc func refresh(sender: UIRefreshControl) {...}

また、リフレッシュをどのように実装するかについては解説記事がたくさん出ているので、そちらを参考にしてもらえれば良いと思います。(簡単にできます)

ただ、リフレッシュに関して1つだけ工夫があるのでそこだけ説明したいと思います。

通常、リフレッシュではインジケータ(くるくる回るやつ)が表示されてしまいます。
しかしアニメーションを見せるのには邪魔ですよね。
なので、見えないようにします。

具体的には以下のようにしてインジケータを透明に設定します。

 refreshCtl.tintColor = .clear

UIImageViewをロード時に固定表示し、処理後に初期状態に戻す

ロード時はGIFアニメーションが見えるように、UITableViewを引っ張ったままの状態で表示した方が良いですよね。
もちろんロード処理が終わった後は元に戻してあげないといけません。


UIImageViewを見えるように固定する方法は以下のように表現されます。

 myTableView.contentInset.top = 150

これはTableViewの上に150分の余白を与えるという意味です。
150というのはヘッダービューにおけるHeaderUIImageViewの高さの和ですね。

ヘッダービューの高さそのものではないことに注意してください。
ヘッダービューの高さに余裕を持たせておいてある可能性もありますから。
(ヘッダービューと背景の色を異なるものにしている場合、スクロールした際にUIImageのすぐ上に背景が見えるのを防ぐため)

こうすることでTableViewの上にくっつくようにして配置されているヘッダービューのヘッダーとGIFアニメーションが見えるようになります。


ロード処理が完了した後に、徐々にヘッダービューを閉じていく方法は以下のように表現されます。

DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
            UIView.animate(withDuration: 0.5, delay: 0.0, options: [],animations: {
                self.myTableView.contentInset.top = 30
            }, completion: nil)
        }

これは、2.5秒後に0.5秒かけてTableViewの余白を30にするという意味です。

つまりDispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {...}を利用して2.5秒間GIFアニメーションを表示し、
UIView.animateでヘッダビューの位置をアニメーションして戻していくようにしているということですね。

ソースコード

主要なソースコードは以下に載せておきます。

Githubにサンプルをあげておくので参考にしてみてください。
このサンプルは実行例にあるものとは違います。(わかりやすくするため。要望があれば実行例の説明もしたいと思います。)
https://github.com/Hajime-Ito/SampleRefreshAnimation

    @objc func refresh(sender: UIRefreshControl) {
        self.addHeaderViewGif()
        myTableView.contentInset.top = 130
        sender.endRefreshing()
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            UIView.animate(withDuration: 0.5, delay: 0.0, options: [],animations: {
                self.myTableView.contentInset.top = 30
            }, completion: nil)
            self.updateHeaderView()
        }
    }

    private func createHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
        myHeaderView.alpha = 1
        myHeaderView.backgroundColor = .white
        myTableView.addSubview(myHeaderView)
        let myLabel = UILabel(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
        myLabel.text = "header"
        myLabel.textAlignment = .center
        myLabel.textColor = UIColor(red: 95/255, green: 158/255, blue: 160/255, alpha: 1)
        myLabel.alpha = 1
        myHeaderView.addSubview(myLabel)
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.image = UIImage(named: "bili")
        myHeaderView.addSubview(image)
    }

    private func updateHeaderView() {
        let displayWidth: CGFloat! = self.view.frame.width
        myHeaderView.subviews[1].removeFromSuperview()
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.image = UIImage(named: "bili")
        myHeaderView.addSubview(image)
    }

    private func addHeaderViewGif() {
        let displayWidth: CGFloat! = self.view.frame.width
        let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
        image.loadGif(name: "bili")
        myHeaderView.addSubview(image)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            self.myHeaderView.subviews[1].removeFromSuperview()
        }
    }

終わりに

ここまで読んでくれたみなさんお疲れ様でした。
ちょっとした工夫がたくさん必要になるような実装でしたね。

しかし、良いと思うものを再現するのは面白いものです。。

実はサンプルプログラムではこの記事では説明しきれなかったプログラムを使用しています。
役に立つと思うので、そちらもチェックしてみてください。

[Swift5]ニコニコ動画やLINEにあるようなスクロールによって閉じたり開いたりするヘッダー

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む