20200212のSwiftに関する記事は9件です。

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

はじめに

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

対象とする読者

自分
SwiftUI初心者

環境

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

@Stateを1枚で説明すると

@State.png

解説

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

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

Xcode の FileMerge.app で Swift のシンタクスハイライトを復活させる

はじめに

macOS の開発ツールである Xcode には開発に役立つツールが色々と入っていて、その中に diff やマージ作業を GUI で行う FileMerge.app というツールがある。「なんちゃら code を立ち上げるまでもないなぁ」というときに、opendiff file1 file2 でターミナルからサクッと立ち上げられるので、個人的には重宝している。左右のペインで2つのファイルを見比べることができるのはもちろん、行単位だけでなく文字単位で差分をハイライトしてくれるので、差分が把握しやすい。

ところが、いつの頃からか(Xcode 9 ぐらいではちゃんと動いていたと思う)、Swift ソースコードを指定して opendiff を起動すると、ターミナルに大量のエラーメッセージが吐き出されるようになり、シンタクスハイライトや関数や変数の宣言の選択ができなくなってしまった(とりあえず、標準エラー出力を /dev/null にリダイレクトする alias をして使っているが)。

Xcode のベータを含む新しいバージョンが出るたびに、Feedback Assistant で報告しているし、他の人も Apple Developer Forums で報告しているのだが、一向に直してくれる気配がない。原因はなんとなく分かっていたので、直してみることにした。

環境

32bit環境が手放せず、いまだに Mojave. ?

  • macOS 10.14.6 (18G3020)
  • Xcode 11.3.1 (11C505)

原因

opendiff で吐き出されるエラーメッセージは以下のような感じ。

$ opendiff file1.swift file2.swift
2020-02-11 18:24:52.980 FileMerge[59271:5815923] Couldn't load language spec for '<DVTSourceCodeLanguage:0x7fd1912b2340:'Xcode.SourceCodeLanguage.Swift'>'
...
(同じエラーメッセージが延々と吐き出される)
...

Swift の language spec が読み込めないと怒られている。Objective C などでは怒られることはない。試しに、Objective C の language spec とやらを探してみる。こういう場合、Rust 製の fd という高速版 find コマンドが便利。

$ cd /Applications/Xcode.app/Contents
$ fd lang | grep -i spec | grep -i objective | grep -vi c++
SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/ObjectiveC.xclangspec
SharedFrameworks/SourceModel.framework/Versions/A/Resources/LanguageSpecifications/ObjectiveC.xclangspec
Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/ObjectiveC.xclangspec
Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/ObjectiveC.xclangspec
Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/ObjectiveC.xclangspec
Developer/Platforms/MacOSX.platform/Developer/iOSSupport/Library/PrivateFrameworks/DVTFoundation.framework/Versions/A/Resources/ObjectiveC.xclangspec

.xclangspec という拡張子を持つファイルが language spec らしいので、Swift のものがあるかどうかを探してみる。

$ fd -e xclangspec | grep -i swift
SharedFrameworks/SourceModel.framework/Versions/A/Resources/LanguageSpecifications/Swift.xclangspec
SharedFrameworks/SourceModel.framework/Versions/A/Resources/LanguageSpecifications/SwiftDocumentationMarkup.xclangspec

Swift の language spec は存在するようだが、Objective C の場合と比べてみると、DVTFoundation.framework に Swift の language spec が存在しないのが原因のようだ。

適当に直してみる

SharedFrameworks/SourceModel.framework/Versions/A/Resources/LanguageSpecifications/Swift.xclangspec を SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/ にコピーしてみる。そうすると...

$ opendiff file1.swift file2.swift
2020-02-11 19:08:56.115 FileMerge[59516:5884912] [MT] DVTSourceScanner: Couldn't load language spec for 'xcode.lang.comment.recursive' (from '')

エラーが1つに減った。DVTFoundation.framework の下にある他の言語の language spec ファイルをみても、xcode.lang.comment.recursive という記述はないので、コピーしてきた Swift.xclangspec 中の xcode.lang.comment.recursive という記述がある行を // でコメントアウトする。

$ diff -U0 Swift.xclangspec,bak Swift.xclangspec
--- Swift.xclangspec,bak    2020-02-11 18:53:48.000000000 +0900
+++ Swift.xclangspec    2020-02-11 19:09:56.000000000 +0900
@@ -196 +196 @@
-                //"xcode.lang.comment.recursive",
+                "xcode.lang.comment.recursive",
@@ -218 +218 @@
-                //"xcode.lang.comment.recursive",
+                "xcode.lang.comment.recursive",
$ opendiff file1.swift file2.swift
$

とりあえずエラーは出なくなったし、シンタクスハイライトもできているようだ。

おわりに

関数や変数の宣言などが正しく抽出できない場合があるようだし、そもそも、Xcode.app パッケージの中身を直接いじっているので、この修正方法についてはあまりオススメしない。Xcode のライセンス上の問題があるかもしれないし、別な不具合を発生させる可能性もある。

あくまでも、自己責任で。??‍♂️??‍♂️

それにしても、Apple さん、このぐらい、さっさと直してくれないかなぁ...

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

用雙指標來反轉陣列(筆記)

在看別人的解題說明時看到的一個寫法,因為簡單好理解印象很深刻,所以把它記錄下來。

執行的次數則是陣列中元素個數的一半。
對於很大的陣列來說的話,應該會非常有效率。

時間複雜度是 O(n) ,屬於線性時間。

func reverse<T>( _ array: inout [T]) {
    if array.isEmpty { return }
    var start = 0
    var end = array.count - 1

    while start < end {
        let temp = array[start]
        array[start] = array[end]
        array[end] = temp

        start += 1
        end -= 1
    }
}

稍微重構

抽 swap 出來

func reverse<T>( _ array: inout [T]) {
    if array.isEmpty { return }
    var start = 0
    var end = array.count - 1

    while start < end {
        swap(&array, start, end)

        start += 1
        end -= 1
    }
}

func swap<T>(_ array: inout [T], _ a: Int, _ b: Int) {
    if a >= array.count || b >= array.count {
        // Do nothing or assert with message over here
        return
    }
    let temp = array[a]
    array[a] = array[b]
    array[b] = temp
}

用用看

來餵個簡單的 test case

var array = [1, 3, 4, 5]
reverse(array)

執行完之後, array 就會變成 [5, 4, 3, 1]

圖解

[1, 3, 4, 5]
 s        e  (s=start) (end)

// swap the values
[5, 3, 4, 1]
 s        e

// moving the pointers
[5, 3, 4, 1]
    s  e

// swap the values
[5, 4, 3, 1]
    s  e

以上。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【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で続きを読む

storyboardで配置したUIButtonの下にコードで位置指定したUIButtonを配置する

今までUILabelやUIButtonをコードで表示する事が怖くて(?)分かりやすいstoryboardを使っていましたが、今回コードで表示することに挑戦してみました。

完成形

スクリーンショット 2020-02-12 2.41.14.png
黒背景のButtonが(centerButtonと命名)storyboardで配置したもので、赤背景のButton(underButtonと命名)がコードで表示したButtonです。Viewの中心に配置したcenterButtonの8ポイント下にunderButtonを配置します。

centerButtonをViewControllerと紐付け

ViewController.swift
 import UIKit

class ViewController: UIViewController {

@IBOutlet weak var centerButton: UIButton!

override func viewDidLoad() {
   super.viewDidLoad()
}

centerButtonのサイズ、座標を調べる

ViewController.swift
//centerButtonの幅
let centerButtonWidth = self.centerButton.frame.width 
//centerButtonの高さ
let centerButtonHeight = self.centerButton.frame.height 
//centerButtonの左端の座標
let centerButtonX = self.centerButton.frame.minX 
//centerButtonの下端の座標
let centerButtonY = self.centerButton.frame.maxY 

iPhoneの座標は左上がx: 0,y: 0なので
・minX = 左端
・maxX = 右端
・minY = 上端
・maxY = 下端
となります。
iPhone座標.png
【iOSアプリ開発】Viewの座標やサイズを取得するノウハウから引用

underButtonをインスタンス化し、位置指定

ViewController.swift
let underButton = UIButton()

underButton.frame = CGRect(x: centerButtonX, y: centerButtonY + 8,
                           width: centerButtonWidth, height: centerButtonHeight)

あとは背景色などを書き足してViewに追加すれば完成

ViewController.swift
// ボタンのタイトルを設定
underButton.setTitle("Button", for:UIControl.State.normal)

// タイトルの色
underButton.setTitleColor(UIColor.white, for: .normal)

// ボタンのフォントサイズ
underButton.titleLabel?.font =  UIFont.systemFont(ofSize: 36)

// 背景色
underButton.backgroundColor = UIColor.red

// Viewにボタンを追加
self.view.addSubview(underButton)

全体のコード

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var centerButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()

        let underButton = UIButton()
        let centerButtonWidth = self.centerButton.frame.width
        let centerButtonHeight = self.centerButton.frame.height
        let centerButtonX = self.centerButton.frame.minX
        let centerButtonY = self.centerButton.frame.maxY

        underButton.frame = CGRect(x: centerButtonX, y: centerButtonY + 8, width: centerButtonWidth, height: centerButtonHeight)

        // ボタンのタイトルを設定
        underButton.setTitle("Button", for:UIControl.State.normal)

        // タイトルの色
        underButton.setTitleColor(UIColor.white, for: .normal)

        // ボタンのフォントサイズ
        underButton.titleLabel?.font =  UIFont.systemFont(ofSize: 36)

        // 背景色
        underButton.backgroundColor = UIColor.red

        // Viewにボタンを追加
        self.view.addSubview(underButton)
    }

}

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む