- 投稿日:2020-02-12T22:56:36+09:00
Swift Stateとはなんぞや1コマ劇場
はじめに
自分のSwift学習メモです。
1枚のメモ用紙に、その宣言や使い方のざっくりしたイメージを記録していきます。対象とする読者
自分
SwiftUI初心者環境
私がこの記事を書いている際に利用しているのは次の環境です。
Xcode 11.3.1
Swift 5.0
@State
を1枚で説明すると解説
変数に
@State
をつけることで、State構造体という特別な場所に変数は格納されます。そして、この変数の値が書き換わるたびに、State構造体に定義されているupdate()が呼び出され、関連する各Viewの再表示が行われます。
状態変更による個別Viewの再表示イベントの実装から解放される模様。
- 投稿日:2020-02-12T19:27:55+09:00
作りながら学ぶ! SwiftUIアニメーション インジケーター編
はじめに
作りながら学ぶ! アニメーション インジケーター編です。
作ろう
円を作る
まずは土台となる円を作ります。
import SwiftUI struct ContentView: View { var body: some View { Circle() } }円をくり抜いて輪を作る
円を輪にするには
stroke
メソッドを使います。func stroke<S>(S, lineWidth: CGFloat) -> View func stroke<S>(S, style: StrokeStyle) -> View func stroke(lineWidth: CGFloat) -> Shape func stroke(style: StrokeStyle) -> Shapehttps://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) } }これでは線が細くて見づらいので、線の幅を広げて見やすいようにしましょう。
そのためにStrokeStyle
を当てていきます。StrokeStyle
境界線または仕切りの色、幅、およびスタイルを定義するオブジェクト。
https://developer.apple.com/documentation/apple_news/strokestyleStrokeStyle(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)) } }円のままだと、回転してもわからないので
trim
メソッドを使ってトリミングしていきます。
trim(from:to:)
func trim(from startFraction: CGFloat = 0, to endFraction: CGFloat = 1) -> some Shapehttps://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)) } }現在のままだと円が大きいので、
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) } }インジケーターっぽいサイズになってきました!
角が尖っているのを、丸くしていきます。
そのためにStrokeStyle
のlineCap
を利用します。CGLineCap (iOS 2.0+)
CGLineCap 線の端 末端 画像 .butt 四角 指定されたパスのエンドポイントまで .round 丸 指定されたパスの端点を超える .square 四角 指定されたパスの端点から線幅の半分だけ超える 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) } }CGLineJoin (iOS 2.0+)
以下のように角がある図形だと変化がわかります。
しかし、今回作成するインジケーターは円弧なので軽く流します。
.miter .round .bevel lineJoin 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) } }dashPhase
破線の開始位置を変更します。デフォルトは0
回転させよう
いよいよアニメーションです。
2次元の回転系のアニメーションには、rotatioinEffect
を使います。
rotationEffect(_:anchor:)
func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some Viewhttps://developer.apple.com/documentation/swiftui/text/3276966-rotationeffect
angle
には回転する角度を指定します。
anchor
には回転する中心を指定します。デフォルトは.center
です。Angle
https://developer.apple.com/documentation/swiftui/angle
Angleにはdegrees
とradians
を渡すことができます。
今回は角度を渡すことにするので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)) } }しかしこれでは、アニメーションとは言えません。
ですので、時間経過とともに変化するようにしていきます。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 デフォルト linear 直線的 一定の割合 easeIn 徐々に早くなる easeOut 徐々に遅くなる easeInOut 開始は遅く、中盤で加速し終盤でまた遅くなる 今回は一定速度で回り続けて欲しいので
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 false リバースしなくて良いので、
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 線形 Angular 円形 Radical 放物状
.stroke
メソッドの中のColor.green
をAngularGradient(gradient: Gradient(colors: [.gray, .white])
に変更します。
そうすると以下のように破線の位置がずれていることが確認できます。そこで
StrokeStyle
のdashPhase
の値をかえます。
dashPhase: 8
にすることで、綺麗に描画されました。
完成済みソースコード
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
- 投稿日:2020-02-12T18:43:33+09:00
プログラミング初心者がSwiftでマルバツゲームのiOSアプリ作ってみた
はじめに
研究室の同期たちとの第三回ハッカソンが開催されたのでその記録。
他のメンバーの記事はこちら。
指定したユーザーのツイートをしゃべるRaspberry Piの作り方
位置情報を使ってSlackのステータスを更新してみた
はじめはtemiロボット用のアプリを作ろうと思ったが、色々うまくいかず断念...
そこで、今回は研究用にiOSアプリを作るためなんとなくSwiftを勉強した記憶を思い出しつつ何か作ってみる。
今回は、夏に作りかけていた簡単なマルバツゲームを完成させて、追加要素としてマルバツゲームの盤面のベクトルを変更できるようにしてみる。以下のサイトが参考になった。
Swiftで初めてiPhoneゲーム開発する人向チュートリアル(マルバツゲームを例にStep by Step解説)準備編
準備したもの
MacBook Air
iPad Pro
Xcode 10.3
Swift 5.0
マルバツゲームの盤面の画像
マル・バツの画像プロジェクトの作成
Xcodeを起動して、「Single View App」でプロジェクト作成。
保存場所とかは好きなところで、プロジェクト名は適当に「MyGame」にした。
アプリ画面の準備
1.画面左上の「ProjectNavigater」からMain.storyboardファイルを選択。
2.画面右上の「Show the Object Library」から、「Button」や「Text Field」、「Image View」など必要なものをstoryboard画面にドラッグ&ドロップで配置。
3.画面右上のアイコン「Show the Assistant editor」をクリックして、storyboard画面の隣に「ViewController.swift」を表示。
4.それぞれ配置したオブジェクトをクリックして、青くなった状態でCtrlキーを押しながらViewControllerへドラッグ&ドロップする。出てきたポップアップ上でコネクション情報を設定。
5.nameをそれぞれに対応したものに設定を完了。以下の例はターンを表示するラベルのText Field。
ViewController.swift@IBOutlet weak var turnLabel: UILabel!コネクションをそれぞれのオブジェクトで設定して画面は完成。
実装編でそれぞれの機能を作っていく。
いるものを全部配置したらこんな感じ。
実装編
マルバツゲームを作ってみる
まずはシンプルなマルバツゲームを作成する。
全体を通して思いつきでやってる部分もあるのでかなり雑なコードになった...各マスの座標を設定したり、マル・バツを置くたびにターンを変更したりする。
さらに、それぞれのマスのマル・バツを更新する関数を作る。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.swiftfunc 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 } } }とりあえずここまではこんな感じ。
進めていくと
こんな感じで勝ちのプレイヤーが表示される。マルバツゲームの盤面のベクトルを変えれるようにする
上下左右に対応したボタンを押すと、その方向にベクトルがかかって一度置いたマル・バツがずれるようにしたい。
なんか重力操れるみたいでかっこいいよね。「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 = [] /*-------------------------------*/ }上下左右、他の方向分の関数も作成。
完成
とりあえず、完成。
動かしてみるとこんな感じ。
ここで「up」ボタンを押すと
こんな風に上にマル・バツがずれる。
勝利条件のチェックも問題なくできるので、とりあえずこれでok。終わりに
久しぶりにSwift触ってみたけど、自分のアイデアとかが目に見える形で実際に動くのは面白い。
まだUIとかボタンの制約とかが荒削りなので、今後これをもっと作り込みたい。以上、第三回研究室ハッカソン備忘録。
- 投稿日:2020-02-12T16:40:45+09:00
[iOS]カスタムドキュメントのサムネイルを表示する - Thumbnail Extension
こんにちは。
@gachalatteです。今回は、カスタムドキュメントのサムネイルを表示する
Thumbnail Extension
を紹介します。Thumbnail Extension
の実装は難しいものではありませんが、インターネットで検索してもほとんど情報が得られない状況ですので、ここにまとめておきます。前回の記事とあわせてご覧ください。
プロジェクトにThumbnail Extensionを追加する
- Xcodeのメニューから、
File
>New
>Target...
を選択します。Application Extension
>Thumbnail Extension
を選択します。Product Name
にThumbnailExtension(任意)
を入力します。Info.plistにカスタムドキュメントを登録する
DocumentBaseApp
>Targets
>ThumbnailExtension
>Info
を開きます。NSExtension
>NSExtensionAttributes
>QLSupportedContentTypes
にカスタムドキュメントのUTIを追加します。ThumbnailProviderを実装する
ThumbnailExtension
>ThumbnailProvider.swift
を開いて実装します。ThumbnailProvider
の実装方法が3種類あることが示されていますので、最適な方法を選択してください。ThumbnailProvider.swiftclass 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.swiftclass 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
でもサムネイルが表示されます。参考
- 投稿日:2020-02-12T16:33:00+09:00
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.
キーワード
UIApplicationEndBackgroundTaskErroriOSのログに出ていたエラーについて
このエラーが最近特に気になるのですが、何が原因で起きてるのかわからなかったので調べてみました。XcodeのバージョンはXcode11
OSはiOS13の実機、シミュレーションです。アプリをバックグラウンドに移行させた時にエラーが出る。
調べた結果
Apple開発者フォーラムのによると
https://forums.developer.apple.com/thread/121990iOS 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にあげた時のバグの場合がある?
- 投稿日:2020-02-12T15:24:14+09:00
【Swift】Storyboardとsegueを利用しないデータをパスする方法~~ Pass data without segue in Swift
本記事はSwiftを勉強する時個人的なメモとします.役に立てればうれしいです.
本に書いてあるデータをパスする方法の多くは Storyboard 上から線を引いて,"Identifier" に名前をつけてから segue によりデータをパスする方法となっています.UserDefaultsを使用たデータパス
UserDefaults はローカルにある軽いデータを記憶するのによく使われる.例えばユーザーが入力したIDやパスワードを記憶するため使うのが便利である.手軽にデータの永続化ができるため、多くの場面で利用される機能です.
対応できる型はInt,Float,Double,BOOL,Array,Dictionary,そしてAnyも対応可能です.
注意: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となる.
- 投稿日:2020-02-12T02:58:33+09:00
[Swift5]ビリビリ動画にあるようなリフレッシュする際にGIFアニメーションを見せるビュー
はじめに
TableViewを引っ張ってリロードすることをリフレッシュと呼びますが、
リフレッシュする際にアニメーションを見せることにより、ユーザが待ち時間に感じる退屈を防ぐことができます。
実際に中国大手の動画共有サイト「bilibli」のスマホアプリでも取り入れられています。
(筆者はbilibiliの世界観を創り出すUIをとても気に入っており、UIに困ると再現して取り込んでしまっています。)この記事ではリフレッシュする際にGIFアニメーションを見せるTableViewの作り方を共有したいと思います。
目次
- 環境
- 実行例
- 考え方
- 画面の構成
- ロード時に画面を固定して見せる
- GIFアニメーションを表示する
- UIImageViewをロード時に固定表示し、処理後に初期状態に戻す
- ソースコード
- おわりに
環境
- Xcode 11.2.1
- Swift5
- SwiftGifOrigin 1.7.0
SwiftGifOriginはCocoaPodsから簡単に入れられます。
実行例
考え方
とてもシンプルに実装することができます。
考えることは、画面の構成とロード時の画面表示についてだけです。
(と言っても細かい工夫は必要になってきます。)1.画面構成
画面構成(初期状態)は以下のようになっています。
TableView
があって、その上にUIView
が被さっていることがわかります。
さらにUIView
にはheader
とUIImageView
が被さっていることがわかりますね。この
UIImageView
がGIFアニメーションを表示する領域となります。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) }ヘッダービューを作成して、そこに
UILabel
とUIImageView
を貼り付けているだけですね。
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というのはヘッダービューにおけるHeader
とUIImageView
の高さの和ですね。ヘッダービューの高さそのものではないことに注意してください。
ヘッダービューの高さに余裕を持たせておいてある可能性もありますから。
(ヘッダービューと背景の色を異なるものにしている場合、スクロールした際に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() } }終わりに
ここまで読んでくれたみなさんお疲れ様でした。
ちょっとした工夫がたくさん必要になるような実装でしたね。しかし、良いと思うものを再現するのは面白いものです。。
実はサンプルプログラムではこの記事では説明しきれなかったプログラムを使用しています。
役に立つと思うので、そちらもチェックしてみてください。