20210128のSwiftに関する記事は6件です。

コンソール アプリ向け、簡易版 Web サイト読み込み

極端な例ですが、コンソール アプリで、様々な問題は無視して

let ws = MyWebLoader("https://www.example.com/")
print(ws.data!)  // データを表示

として、サイトのデータを取得したいときの、MyWebLoader クラスです。

注意: サイトが応答するまで処理がブロックされます

class MyWebLoader {
    var sem = DispatchSemaphore(value: 0)
    var error: Error?
    var response: HTTPURLResponse?
    var data: Data?
    init(_ string: String) {
        let url = URL(string: string)!
        let req = URLRequest(url: url)
        let task = URLSession.shared.dataTask(with: req) { dat, res, err in
            self.error = err
            self.response = res as? HTTPURLResponse
            self.data = dat
            self.sem.signal()
        }
        task.resume()
        sem.wait()
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIチュートリアル1を丁寧に説明しながらやってみるよ。

おはよう:relaxed:
今年の目標の一つがiOSアプリ作成してみること、です。
そのために、まずSwiftに入門!ということで、SwiftUIのチュートリアルをやりつつ、気になった部分も調べていきます。

教材

Introducing SwiftUI
このチュートリアルを全部やると、建物・場所について説明をみたり、好きな場所をシェアしたりできるLandmarkアプリができるみたい。

チュートリアル1:Viewを作って、組み合わせてみよう!

Creating and Combining Views
画像やテキストといった要素を組み合わせたViewの作り方を学ぶよ。

Section1:プロジェクトを作ってみよう!

Xcodeを立ち上げるて、新しいプロジェクトを作るよ。
スクリーンショット 2021-01-16 12.05.21.png

スクリーンショット 2021-01-16 12.07.12.png

スクリーンショット 2021-01-16 12.01.49.png

Interface 特徴
SwiftUI WWDC2019で発表された新しいUI実装方法。iOS13以上で使える。コードでUIを実装する。
Storyboard SwiftUIが出てくる前のUI実装方法。GUIでUIを実装する。

今回はSwiftUI。

Life Cycle 特徴
SwiftUIApp WWDC2020で発表。iOS14からのフレームワーク。
UIKitAppDelegate SwiftUI登場以前のフレームワーク。

今回はSwiftUIApp。

最後に、ファイルの保存場所を決めて、完了。

するとこんな感じでファイルが作成されるよ。
チュートリアルにしたがって「Resume」をクリックしたら、しっかりHello worldされたね!
スクリーンショット 2021-01-23 10.11.46.png

ファイルの詳細を見てみるよ。

拡張子
.swift アプリケーションの処理を記入する。Swiftでコードが書かれる。
.xcassets アプリケーションのアイコンなどすべての画像が保存される。
.plist アプリケーションの設定情報を管理する。

LandmarksApp.swift
プロジェクト名+App.swift 形式で自動作成されたファイルだよ。

LandmarksApp.swift
//
//  LandmarksApp.swift
//  Landmarks
//
//  Created by XXXXX on 2021/01/24.
//

import SwiftUI

@main //point1-1
struct LandmarksApp: App { //point1-2 & point1-3
    var body: some Scene { //point1-4
        WindowGroup { //point1-5
            ContentView()
        }
    }
}

point1-1 @main
属性の一つでこれをつけることで、このアプリのエントリーポイントとして指定しているよ。

point1-2 struct=構造体
プログラムを構成する要素・機能を詰んだ構造体だよ。ほぼほぼClassだけれど、違いもあるよ。

【structでもClassesでもできること】
・定数、変数、関数、Arrayやlist型で格納された値にアクセスする際のfruits['apple']のappleのような添え字の定義。
・初期値の設定
・標準機能の提供。

【Classesではできるけれど、structではできないこと】
・継承。
・型キャスト(変数やオブジェクトを別の型に変えること)
・初期化解除の定義。
・参照。(structは値渡し)

ポイントはClassesでは継承やインスタンスの作成ができるけれど、structではそれができないところかな?
参考ページ:ClassesAndStructures

point1-3 struct XXX : ZZZZ
XXXがstructの名前。ZZZがプロトコル名。
今回の場合は「LandmarksApp」は「App」というプロトコルに準拠した構造で宣言しますよー、といっている。多分。
参考ページ:app

point1-4 var body : some Scene
Appのなかのvar bodyはアプリの振る舞いや、要素を設定するところ。
Scanというプロトコルに準拠して作られているよーという宣言の仕方をしている。
Sceneはアプリのユーザーインターフェース部分と思われる。
参考ページ:Scene

ちなみにvarは変数、letは定数の定義に使います。

point1-5 windowGroup
この中でViewの階層を定義する。
今回は、ContentViewを呼び出しているので、アプリが実際に作成している画面はこのContentViewの内容になる・・と思われる。
https://developer.apple.com/documentation/swiftui/windowgroup

windowGroupのなかで呼び出されているContentViewが定義されているContentView.swiftを見てみるよ。

ContentView.swift
//
//  contentView.swift
//  Landmarks
//
//  Created by XXXXXX on 2021/01/24.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Viewというプロトコルに準拠して作られていることがわかるね。
実際の記述内容は以降のセクションで詳しく見ていくよ。

Section2 テキストを編集してみよう!

ディスプレイに表示されている内容を変えてみるよ。
Hello Worldにフォーカスを当てると、テキストの編集メニューが表示されるよ。
スクリーンショット 2021-01-23 10.12.52.png

このメニューを編集すると、編集した内容がコードに反映されて、ディスプレイされる文字も変わるみたい。
テキストとフォントをここから変えて、Resumeをクリックすると画像のようになったよ。
スクリーンショット 2021-01-23 10.17.00.png

Textに対して「.font」「.padding」といった修飾を繋げていくことで、
デザインを変更することができるんだね。
要素が重なってくると順番が大切になります。

  Text("Turtle Rock")
   .font(.title)
   .foregroundColor(.green)
   .padding()
 }
}

Section3 Stackを使って要素を組み合わせてみよう。

今はテキストが中央に1つ置いてあるだけなので、並列してみるよ。
Textの上でCommandキー押しながらクリックすると、追加するアクションが出てくるので、VStackを選択するよ。
スクリーンショット 2021-01-24 11.33.46.png
すると、VstackにTextが囲まれた状態になったね。

次は、Textを追加するよ。XCodeの画面の右上にある+ボタンから、簡単に追加ができるよ。

スクリーンショット 2021-01-24 11.44.27.png

ContentView.swift
struct ContentView: View {
    var body: some View {
        VStack (alignment: .leading) {  // poin3-1
            Text("Turtle Rock")
                .font(.title)
                .foregroundColor(.green)
                .padding()
            HStack {     // point3-2
                Text("Joshua Tree National Park")
                    .font(.subheadline)
                Spacer() 
                Text("California")
                    .font(.subheadline)

            }
        }
        .padding() // point3-3
    }
}

point3-1 VStack(alignment: .leading)
垂直方向、縦に並べたい時に要素を囲む。
今回はTurtleRockとHStackの要素を縦に並べているね。
また後ろについている(alignment〜)はVStackで囲まれた中にある要素の寄せを指定しているよ。今回はleading = 左寄せ。

point3-2 HStack
水平方向、横に並べたい時に要素を囲む。
今回はテキスト2つとその分離に使うSpacerを横並びにしているね。

point3-3 VStackに対する.padding()
paddingはVStack要素に対して付与されてるね。
paddingにかかわらず、修飾子をどこにつけるかによって、それが反映される範囲が変わるので、注意が必要だよ。

Section4 画像を追加してみよう。

まずはチュートリアルセットでダウンロードしたファイルの中にあるjpgを
Projectの中のAssets.xcaassetの中に入れるよ。
スクリーンショット 2021-01-24 12.26.19.png

その後、新しくファイルを作成し、画像に対して編集をしていくよ。
完成後の見た目とコードはこんな感じ。
(輪郭が白だとわかりずらかったため、グレーのままにしてあります。)

スクリーンショット 2021-01-24 12.30.37.png

CircleImage.swift
import SwiftUI

struct CircleImage: View {
    var body: some View {
        Image("turtlerock") //point4-1
            .clipShape(Circle()) // point4-2
            .overlay(Circle().stroke(Color.gray,lineWidth: 4)) // point4-3
            .shadow(radius:7 )
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}


point4-1 Image(XX-画像名-XX)
assetに追加したファイルと同じ名前を指定し、画像要素を作る。

point4-2 clipSharp(図形)
clipSharpは画像の切り取りの指定で、今回は円形に切り取りをしている。

point4-3 overlay
Imageに対し重ねる要素を指定している。今回は同じく円形の輪郭stroke)を重ねている。strokeの後ろには輪郭のスタイルが記載。今回はグレーの幅(lineWidth)が4mm。

Section5 ほかのフレームワークを追加してみよう。

次は、Mapをviewの中に表示するよ。
使用するのは「mapkit」というフレームワークを使用するよ。
出来上がった画面はこんな感じ。ちょっと見づらいけれど、地図だよ。
地図を動かしてみるには画像のまるをつけたところをクリックして、ライブプレビューモードにする必要があるよ。
スクリーンショット 2021-01-28 16.30.27.png

MapView.swift
import SwiftUI
import MapKit

struct MapView: View {
    @State private var region = MKCoordinateRegion( //point5-1
        center: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868),
        span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2) //point5-2
    )
    var body: some View {
        Map(coordinateRegion: $region) //point5-3
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}



point5-1 @State
データバインディングの仕組み。これをつけることで値の更新を監視して、自動で反映してくれる。

point5-2 MKCoordinateRegion(center:XXXXX, span:YYYYY)
MKCoordinateRegionは緯度経度の情報を与えるとそれを中心に、地形情報を返します。
centerとspanの二つの引数をあり、それぞれ下記の通り。
center:領域の中心の緯度経度の指定。latitudeが緯度、longitudeが経度。
span:表示する地図の幅を指定。latitudeDeltaが南北の距離、longitudeDeltaが東西の距離を示す。

point5-3 $region
「$」をつけることで、データバインディングさせることができる。
最初に地図を表示した際は、MKCoordinateRegionをしたときの緯度経度を元に地図を描画をしたね。でも、そのあと、ユーザー側で画面を動かした時も、地図の描画を変える必要がある。そのため、バインディングして、地図に渡される緯度経度の情報が更新、それをつけとった地図側でも描画し直すようになっているよ。

Section6 全部まとめてみよう。

最後は最初に作ったテキスト画面を編集して、地図+画像+テキストの画面を作るよ。
スクリーンショット 2021-01-28 16.45.06.png

ContentView.swift
//
//  ContentView.swift
//  Landmarks
//
//  Created by XXXXXXX on 2021/01/24.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {

            MapView() //point6-1
                .frame(height: 300)
                .ignoresSafeArea(edges:.top) // point6-2

            CircleImage()  //point6-3
                .offset(y: -130)
                .padding(.bottom,-130)

            VStack (alignment: .leading) {
                Text("Turtle Rock")
                    .font(.title)

                HStack {
                    Text("Joshua Tree National Park")
                        .font(.subheadline)
                    Spacer()
                    Text("California")
                        .font(.subheadline)

                }
                .font(.subheadline)  // point6-4
                .foregroundColor(.secondary)

                Divider()

                Text("About Turtle Rock")
                    .font(.title2)
                Text("Descriptive text goes here.")
              }
            .padding()
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
        }
    }
}

point6-1 MapView
Section5で作成したMapView.swiftで定義してある、MapViewを参照。
fram そのviewサイズを指定。本来であれは、widthとheightの両方を指定するけれど、heightだけ指定した場合には、その画面に合わせて自動で幅が決まるよ。今回は画面いっぱいに表示がされているね。

point6-2 ignoresSafeArea
セーフエリアを無視する時につける。これをつける前の表示だと、地図の上に余白が設けられていたよ。これをつけることで、その領域の確保がされなくなるよ。
https://developer.apple.com/documentation/swiftui/view/ignoressafearea(_:edges:)

point6-3 CircleImage
Section4で作成したCircleImage.swiftの中で定義した、CircleImageを参照している。

point6-4 VStackに対する修飾
Stackに対してレイアウトの変更をすると、その中に含まれる全ての要素がそれに准ずるようになるよ。

以上でチュートリアルは終わりです。
理解度テストをやっておしまい。

 まとめ

最初だったので、超丁寧に調べながら進めたよ。
記述方式だけではなく、修飾子やフレームワーク、それぞれのファイルの持ち方も含め、勉強になりました。
一方で、まだ言葉の意味が落とし込めていないものもあるので、これは実際作ってみながら「こういうことね」と納得していけたらいいな:relaxed:

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

UIScrollViewが慣性スクロール中かどうか知る

概要

UIScrollViewがスクロール中かどうかを知りたかったので、方法を調べた。

環境

  • iPadOS14.2
  • iOS14
  • swift

結果

ペンシルのことを考慮しない場合は

UIScrollView.isDecelerating

というプロパティが生えているので、これを使うことができる。
※詳しく調査していないが、以前はこれだけだと問題があったようだが、現時点(iOS14)では(ほぼ)これでうまくいく

ただ、慣性スクロールを指で止めた場合は問題ないが、ペンシルで止めた場合、isDeceleratingがtrueになりっぱなしで困った。

調査の結果、以下のようなワークアラウンドで回避できた。

extension UIScrollView  {
    // こんな処理を用意してあげて
    func stopDecelerating() {
        let contentOffset = self.contentOffset
        self.setContentOffset(contentOffset, animated: false)
    }
}

// 対象のUIScrollViewのtouchesShouldBeginなどをオーバーライドして、
// ペンシルの場合は、明示的にスクロールを止める
class HogeView: UIScrollView {

    override public func touchesShouldBegin(
        _ touches: Set<UITouch>, with event: UIEvent?, in view: UIView
    ) -> Bool {

        if isDecelerating && touches.contains(where: { $0.type == .pencil }) {
            stopDecelerating()
        }
    }
}

参考

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

[SwiftUI]List化したCoreDataを横スワイプで行削除する方法

結構調べたけど、日本語の資料がなかったので備忘録として残しておく。

やりたかったこと

以下のようにCoreDataを参照したリストを横スワイプで削除したかった。
UIの要素だけでなく、元データごと削除する。

tobe.gif

よく他のページで見かける以下のような実装だとCoreDataの型が合わず処理がうまくいかない。

struct ContentView: View {
    @State private var data = ["one", "two", "three"]

    var body: some View {
        List {
            ForEach(data, id: \.self) { data in
                Text(data)
            }
            /// 行削除操作時に呼び出す処理の指定
            .onDelete(perform: rowRemove)
        }
    }

    /// 行削除処理
    func rowRemove(offsets: IndexSet) {
        data.remove(atOffsets: offsets)
    }
}

やったこと

1.CoreDataが使えるようになっているか以下の設定を確認する。

@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(
    entity: TestCoreData.entity(),
    sortDescriptors: [
        NSSortDescriptor(keyPath: \TestCoreData.name, ascending: true),
    ]
) var testCoreData: FetchedResults<TestCoreData>

2.以下のようなリストに対して.onDelete(perform:〜)を追加。

List {
    ForEach(testCoreData, id: \.self) { testCoreData in
        Text("Creator: \(testCoreData.creator ?? "Anonymous")")
    }.onDelete(perform: removeCoreData)
}

3.リストの番号を保持したoffsetsを使って、削除するCoreDataの要素を指定。

func removeCoreData(at offsets: IndexSet) {
    for index in offsets {
        let putTestCoreData = testCoreData[index]
        managedObjectContext.delete(putTestCoreData)
    }
}

以上!!

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

iOS NWConnection利用時のTLSの証明書を無視する方法

はじめに

iPhoneと家庭内で使っているIoT機器とのTLSを使った暗号通信する時に、サーバ証明書の有効性の確認が失敗して、通信エラーになる現象がありました。例えば、IoT機器が自己署名の証明書を使っている場合です。このような場合に、iOSでTLS証明書を無視して通信する方法を書いておきます。

通常の実装

NWConnectionを用いた通常のTLS実装は、以下のようになります。今回の記事に関連する部分(NWConnectionのインスタンス化)のみ記載しています。この実装では、サーバ証明書の確認を行います。このため、IoT機器が自己署名の証明書を使っている場合には、サーバ証明書のエラーとなりIoT機器と通信ができません。

let connection = NWConnection(host: host, port: port, using: .tls)

サーバ証明書を無視する実装

以下のように、NWConnectionのパラメータ(using: params)を設定することで、サーバ証明書を無視することができます。sec_protocol_options_set_verify_block()sec_protocol_verify_complete(true)を設定します。この時に、connection.start(queue: queue)で使うqueueが引数に必要です。このため、対象のコードの上下にqueueに関するコードも記載してあります。

let queue = DispatchQueue(label: "label")

let options = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (_, _, sec_protocol_verify_complete) in
        sec_protocol_verify_complete(true)
    }, queue)
let params = NWParameters(tls: options)
let connection = NWConnection(host: host, port: port, using: params)

// 途中省略
connection.start(queue: queue)

参考文献

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

ユーザーが5回または10回アプリを起動した時にレビューを依頼する方法

はじめに

レビューアラートを表示する方法自体はimport StoreKitを含めてもほんの2行で済むのですが、いつどこで表示させるかが悩みどころ。わざわざレビューのためのボタンを配置するのも億劫なので、特定の回数起動された時に依頼することにしました。

回数をカウントする

Userdefaultsを使って起動回数をカウントします。

let key = "startupCount"
let userDefaults = UserDefaults(suiteName: "/*一意に定まる名前*/")
userDefaults?.setValue((userDefaults?.integer(forKey: key))!+1, forKey: key)
userDefaults?.synchronize()
let count = userDefaults?.integer(forKey: key)

実行された回数を数えるための変数を用意します。僕はこれをアプリが起動された時に実行される関数の中に入れましたが、数えたいものによってはイニシャライザの中に入れても良いと思います。

レビューを表示させる

レビューを表示すること自体はとても簡単です。StoreKitをimportし、表示させたいところに

SKStoreReviewController.requestReview()

と書きます。
今回は起動回数が5または10の時表示させたいので

if count == 5 || count == 10  {
SKStoreReviewController.requestReview()
}

としました。

SceneDelegateに処理を加える

アプリによってはAppDelegateで起動時の処理を行っているものもあると思います。(特にxcode11以前のもの)

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = Tab()
        let key = "startupCount"
        let userDefaults = UserDefaults(suiteName: "group.otsuka.amane.ColorDesignApp")
        userDefaults?.setValue((userDefaults?.integer(forKey: key))!+1, forKey: key)
        userDefaults?.synchronize()
        let count = userDefaults?.integer(forKey: key)


        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
            if count == 5 || count == 10  {
                SKStoreReviewController.requestReview()
            }
        }
    }
//省略
}

なおここで使っているTab()とははじめに表示される画面のことで
スクリーンショット 2021-01-28 10.15.20.jpg

こんな感じで定義しています。詳しくはこちら

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