20200625のiOSに関する記事は13件です。

iOS MultipeerConnectivity Frameworkに必要なデレゲートメソッドたち 【Swift】

はじめに

MCBrowserViewControllerを使わないパターンでのMultipeerConnectivity適用に必要なデレゲートメソッドたちです。

  • iOS 13.5
  • Swift 5.2

準備編

class xxxxxController {

    let displayName: String!
    let serviceType: String!
    var peerID: MCPeerID!
    var session: MCSession!
    var browser: MCNearbyServiceBrowser!
    var advertiser: MCNearbyServiceAdvertiser!

    func setup() {
        displayName = "displayName"
        serviceType = "serviceType"

        peerID = MCPeerID(displayName: displayName)
        session = MCSession(peer: peerID)
        session.delegate = self

        browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        browser.delegate = self
        browser.startBrowsingForPeers()

        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        advertiser.delegate = self
        advertiser.startAdvertisingPeer()
    }
}

SessionDelegate編

extension xxxxxController: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
    }

    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
    }

    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
    }

    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
    }    
}
sendメソッドたち
func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws

func startStream(withName streamName: String, toPeer peerID: MCPeerID) throws -> OutputStream

func sendResource(at resourceURL: URL, withName resourceName: String, toPeer peerID: MCPeerID, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) -> Progress?

MCNearbyServiceBrowserDelegate編

extension xxxxxController: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
    }

    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
    }

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        browser.invitePeer(peerID, to: session, withContext: nil, timeout: 0)
    }

}

MCNearbyServiceAdvertiserDelegate編

extension xxxxxController: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
        invitationHandler(false, nil)
    }

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
    }
}

以上

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

CreateMLでスポーツビデオのAction Classifierをつくる(書きかけ)

1、データセットをつくる。

動画に要求されること。

・各パターンごとに60動画(train50,test5,validation5)ぐらい必要。
・画面の中心に一人の全身がおさまっている。
・1クリップに1動作。前後の余分な動きはカットで収録されている。
・動画のフレームレート(1秒間に何コマか)は統一すべき。
・休憩や関係ない運動など、分類したい動作以外の「他の動作」動画も1分類として用意。

分類したい動作ごとにフォルダに入れ、フォルダの名前を動作名にする。

(このフォルダの名前が、分類結果の名前として出てくる)。

スクリーンショット 2020-06-25 20.00.29.png

スクリーンショット 2020-06-25 20.00.19.png

2、CreateMLでAction Clasifierモデルをトレーニングする。

XcodeのDeveloper ToolsからCreateMLをひらく。
スクリーンショット 2020-06-25 19.31.38.png

VideoセクションのAction Classificationを選択。
スクリーンショット 2020-06-25 19.32.49.png

Training Dataで、用意したデータセットフォルダを指定する。
スクリーンショット 2020-06-25 19.37.43.png

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

NSAttributedStringをかんたんに生成する拡張関数(Swift)

はじめに

iOSアプリ開発では、複数行に渡る文章の行間( lineSpacing )など、文字列に複雑な装飾を施す場合、 NSAttributedString を生成する必要があります。
生成方法が複雑なので、引数を渡してかんたんに生成する拡張関数を実装しました。

環境

  • OS:macOS Mojave 10.14.6
  • Xcode:11.3.1 (11C504)
  • Swift:5.1.3

実装

イニシャライザを拡張関数として定義します。

NSAttributedString+Init.swift
import UIKit

extension NSAttributedString {
    convenience init(font: UIFont, color: UIColor, lineSpacing: CGFloat, alignment: NSTextAlignment, string: String) {
        var attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: color
        ]
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineSpacing = lineSpacing
        paragraphStyle.alignment = alignment
        attributes.updateValue(paragraphStyle, forKey: .paragraphStyle)

        self.init(string: string, attributes: attributes)
    }
}

使い方

拡張関数として定義したイニシャライザを呼び出すのみです。

after
    @IBOutlet private weak var descriptionLabel: UILabel! {
        willSet {
            newValue.attributedText = NSAttributedString(
                font: .systemFont(ofSize: 16.0),
                color: .blue,
                lineSpacing: 6.0,
                alignment: .left,
                string: "description"
            )
        }
    }

拡張関数の定義前と比べると、スッキリ書けることがわかります。

before
    @IBOutlet private weak var descriptionLabel: UILabel! {
        willSet {
            var attributes: [NSAttributedString.Key: Any] = [
                .font: UIFont.systemFont(ofSize: 16.0),
                .foregroundColor: UIColor.blue
            ]
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = 6.0
            paragraphStyle.alignment = .left
            attributes.updateValue(paragraphStyle, forKey: .paragraphStyle)

            newValue.attributedText = NSAttributedString(string: "description", attributes: attributes)
        }
    }

おわりに

今回紹介した拡張関数は、私が必要としている最低限の引数のみ追加しているので、必要に応じてカスタマイズしてください。
NSMutableAttributedString として可変に扱う場合にもカスタマイズが必要です。

おまけ:他の方法

Twitterで他の人がどう NSAttributedString を生成しているか伺ったので、紹介します。

NSAttributedString を定数化する

行間の大きさが画面間で変わることも少ないので、 NSAttributedString ごと定数化するのはありだと思いました。
定数化の実装時に私が紹介した拡張関数を使うと、よりスッキリ書けると思います。

String から生成できるようにする

StringNSAttributedString を返す拡張関数を追加する方法です。
こちらもわかりやすくていいと思いました。
ただ文字列をローカライズしている場合、若干読みづらくなるかもしれません。

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

【Swift5】MapKitまとめ

MapKitに関する最新の情報が少ないように感じたので
よく使うような基本的な使い方についてまとめました!

今回の実装コードはGithubにあげているので自由にどうぞ!
Delegate部分はextentionで実装しているので多少異なります。
https://github.com/tomoki-inoue1221/mapkit-sample

網羅している内容

基本編

  • MapKitを使用するための設定と注意点
  • 現在地の表示
  • 現在地への照準を合わせる
  • 指定した場所にピンを立てる(タイトルやサブタイトルの設定)
  • ピンがタップされた時の処理
  • ロングタップでタップした場所にピンを立てる

応用編

  • 住所から緯度・経度を取得する(ジオコーディング)
  • 緯度・経度から住所を取得する(逆ジオコーディング)
  • カスタムピンの表示

基本編の実装

MapKit使用するための事前知識

現在地を表示する設定

現在地を表示するためには2点設定が必要です。

  1. Info.plistに許可設定用の内容を記述
  • Privacy - Location When In Use Usage Description
  • Privacy - Location Always Usage Description image.png
  1. storyBoardでMapViewのUserLocationにチェックを入れる image.png

現在地を表示する注意点

現在地を表示するためには実機での確認が必要です。

LocationManagerについて

Mapを使う上ではLocationManagerの存在は避けて通れないので、
こちらを一読しておくと良いです!
【CoreLocation】位置情報を取得する

よく使うDelegateメソッドの紹介

  • CLLocationManagerDelegate編
sample.swift
/// delegateをセットする呼ばれる(位置情報の許可設定に使用)
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {}

/// 自分の現在地が更新された時に呼ばれる(現在地更新した時に何か処理したい場合に使用)
/// locationsに現在地入ってる
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {}
  • MKMapViewDelegate編
sample.swift
/// ピンを追加した時に呼ばれる(ピンを加工したりする)
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {}

/// ピンをタップした時に呼ばれる(ピンの詳細情報を出したりする)
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {}

現在地の表示

位置情報の許可を求める

  • コードの実装
ViewController.swift
import UIKit
// 2つimportする
import MapKit
import CoreLocation

// CLLocationManagerDelegateを継承する
class ViewController: UIViewController, CLLocationManagerDelegate {
  // storyboardから接続する
  @IBOutlet weak var mapView: MKMapView!
  // locationManagerを宣言する
  var locationManager: CLLocationManager!

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     locationManager = CLLocationManager()
     locationManager.delegate = self
     locationManager!.requestWhenInUseAuthorization()
  }

 // 許可を求めるためのdelegateメソッド
 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        // 許可されてない場合
        case .notDetermined:
       // 許可を求める
            manager.requestWhenInUseAuthorization()
        // 拒否されてる場合
        case .restricted, .denied:
            // 何もしない
            break
        // 許可されている場合
        case .authorizedAlways, .authorizedWhenInUse:
            // 現在地の取得を開始
            manager.startUpdatingLocation()
            break
        default:
            break
        }
    }
}

こちらで、Info.plist・storyboardの設定がうまくできていれば
許可を求めるダイアログが表示されます。
S__7512083.jpg

許可を完了するとマップに現在地が表示されます。
S__7512084.jpg

現在地に照準を合わせる

  • コードの実装
ViewController.swift
import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
  @IBOutlet weak var mapView: MKMapView!
  var locationManager: CLLocationManager!

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     // 省略

   // 現在地に照準を合わす
     // 0.01が距離の倍率
     let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
     // mapView.userLocation.coordinateで現在地の情報が取得できる
     let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, span: span)
     // ここで照準を合わせている
     mapView.region = region
  }

}

初期の照準位置を変更したい場合は、
mapView.userLocation.coordinateを変更すればよくて、
例えば東京駅に合わせたければ、
東京駅の緯度・経度を調べて

ViewController.swift
// 省略

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     // 省略

   // 東京駅に照準を合わす
     let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
     // 東京駅の位置情報をセット
     let tokyoStation = CLLocationCoordinate2DMake(35.681236, 139.767125)
     // centerに東京駅のlocationDataをセット
     let region = MKCoordinateRegion(center: tokyoStation, span: span)
     mapView.region = region
  }

このように書くと東京駅に照準がアイマス。

指定した場所にピンを立てる

  • コードの実装
ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // ロケーションマネージャーのセットアップ

        // 現在地に照準を合わす


        // 指定値にピンを立てる
        // ピンを立てたい緯度・経度をセット
        // let coordinate = CLLocationCoordinate2DMake(35.45, 139.56)
        // 今回は現在地とする
        let coordinate = mapView.userLocation.coordinate
        // ピンを生成
        let pin = MKPointAnnotation()
     // ピンのタイトル・サブタイトルをセット
        pin.title = "タイトル"
        pin.subtitle = "サブタイトル"
        // ピンに一番上で作った位置情報をセット
        pin.coordinate = coordinate
        // mapにピンを表示する
        mapView.addAnnotation(pin)
   {

ピンがタップされた時の処理

ViewController.swift
// MKMapViewDelegateを継承
class ViewController: UIViewController,CLLocationManagerDelegate,MKMapViewDelegate {
    @IBOutlet weak var mapView: MKMapView!
    var locationManager: CLLocationManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        // 省略

        // delegateをセット
        mapView.delegate = self
  }

    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        // タップされたピンの位置情報
        print(view.annotation?.coordinate)
        // タップされたピンのタイトルとサブタイトル
        print(view.annotation?.title)
        print(view.annotation?.subtitle)
    }

}

よくあるのはタップした時にモダール的な感じで、ピンの詳細がでてくるみたいな画面
GoogleMapで言うとこの井の頭自然文化公園の詳細情報みたいな感じを出すことが多いかも
S__7512085.jpg

ロングタップでタップした場所にピンを立てる(位置情報も取得)

  • コードの実装
ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        //  省略

        // ロングタップを検知
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(recognizeLongPress(sender:)))
        //MapViewにリスナーを登録
        self.mapView.addGestureRecognizer(longPress)
    }

    //ロングタップした時に呼ばれる関数
    @objc func recognizeLongPress(sender: UILongPressGestureRecognizer) {
        //長押し感知は最初の1回のみ
        if sender.state != UIGestureRecognizer.State.began {
            return
        }

        // 位置情報を取得
        let location = sender.location(in: self.mapView)
        let coordinate = self.mapView.convert(location, toCoordinateFrom: self.mapView)
        // 出力
        print(coordinate.latitude)
        print(coordinate.longitude)
        // タップした位置に照準を合わせる処理
        let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        self.mapView.region = region

        // ピンを生成
        let pin = MKPointAnnotation()
        pin.title = "タイトル"
        pin.subtitle = "サブタイトル"
        // タップした位置情報に位置にピンを追加
        pin.coordinate = coordinate
        self.mapView.addAnnotation(pin)
    }

ここで応用編でやる、逆ジオコーディングを使うと住所も取得できる。

応用編の実装

住所から緯度・経度を取得する(ジオコーディング)

よく使うのは住所検索した時に、検索位置に移動してピン立てるみたいな動き
※今回は簡略化のため、入力された想定で固定の住所から緯度・経度を取得する

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 画面に適当にボタンを配置する
    @IBAction func tap(_ sender: Any) {
        geoCording()
    }


    // ジオコーディング(住所から緯度・経度)
    func geoCording() {
        // 検索で入力した値を代入(今回は固定で東京駅)
        let address = "東京都千代田区丸の内1丁目"
        var resultlat: CLLocationDegrees!
        var resultlng: CLLocationDegrees!
        // 住所から位置情報に変換
        CLGeocoder().geocodeAddressString(address) { placemarks, error in
            if let lat = placemarks?.first?.location?.coordinate.latitude {
                // 問題なく変換できたら代入
                print("緯度 : \(lat)")
                resultlat = lat

            }
            if let lng = placemarks?.first?.location?.coordinate.longitude {
                // 問題なく変換できたら代入
                print("経度 : \(lng)")
                resultlng = lng
            }
            // 値が入ってれば
            if (resultlng != nil && resultlat != nil) {
          // 位置情報データを作成
                let cordinate = CLLocationCoordinate2DMake(resultlat, resultlng)
                let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
          // 照準を合わせる
                let region = MKCoordinateRegion(center: cordinate, span: span)
                self.mapView.region = region

                // 同時に取得した位置にピンを立てる
                let pin = MKPointAnnotation()
                pin.title = "タイトル"
                pin.subtitle = "サブタイトル"

                pin.coordinate = cordinate
                self.mapView.addAnnotation(pin)
            }
        }
    }
}

これでボタンをタップした時に東京駅に照準があってピンが立つ

緯度・経度から住所を取得する(逆ジオコーディング)

よく使われるのは、基本編でやったロングタップした時に
そこの住所を出すみたいな動き

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 画面に適当にボタンを配置する
    @IBAction func tap(_ sender: Any) {
        reverseGeoCording()
    }


    // 逆ジオコーデインング
    func reverseGeoCording() {
        // 住所を取得したい位置情報を宣言(今回は東京駅にセット)
        let location = CLLocation(latitude: 35.681236, longitude: 139.767125)
        // 位置情報から住所を取得
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
            guard let placemark = placemarks?.first, error == nil else { return }
            // 市区町村より下の階層が出力
            print(placemark.name!)
            // 都道府県
            print(placemark.administrativeArea!)
            // なんとか郡とかがあれば(ない場合もあるのでnull回避)
            print(placemark.subAdministrativeArea ?? "")
            // 市区町村
            print(placemark.locality!)
            // これで日本語の住所はいい感じにでる
            print(placemark.administrativeArea! + placemark.locality! + placemark.name!)
        }
    }
}

placemarkにはいろんな値が入ってるので、
リファレンスはこちらの記事がわかりやすかったので参考に!
[iOS] MapKitを使って”ジオコーディング・逆ジオコーディング”をやってみる

カスタムピンの表示

よくある自分のオリジナルのピンを表示したりする感じです。
APIなどで地図に表示するデータを取得して表示する、みたいな時に使う感じになると思います。

ViewController.swift
import UIKit
import MapKit
import CoreLocation

// MKPointAnnotation(要するにピン)を継承したカスタムクラスを作成
class MapAnnotationSetting: MKPointAnnotation {
    // デフォルトだとピンにはタイトル・サブタイトルしかないので、設定を追加する
    // 今回は画像だけカスタムにしたいので画像だけ追加
    var pinImage: UIImage?
}



class ViewController: UIViewController,CLLocationManagerDelegate,MKMapViewDelegate {
    @IBOutlet weak var mapView: MKMapView!
    var locationManager: CLLocationManager!

    // とりあえずテストデータで画像・タイトル・サブタイトル・位置情報を用意
    let pinImagges: [UIImage?] = [UIImage(named: "inu1"),UIImage(named: "inu2")]
    let pinTitles: [String] = ["白いい犬","茶色い犬"]
    let pinSubTiiles: [String] = ["比較的白いです","茶色いのが売りです"]
    let pinlocations: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(35.68, 139.56),CLLocationCoordinate2DMake(35.70, 139.56)]

    override func viewDidLoad() {
        super.viewDidLoad()

        // 省略

        // カスタムピンの表示
        // for文で配列の値を回す(ここはいろんなやり方があると思います。)
        for (index,pinTitle) in self.pinTitles.enumerated() {
            // カスタムで作成したMapAnnotationSettingをセット(これで画像をセットできる)
            let pin = MapAnnotationSetting()

            // 用意したデータをセット
            let coordinate = self.pinlocations[index]
            pin.title = pinTitle
            pin.subtitle = self.pinSubTiiles[index]
            // 画像をセットできる
            pin.pinImage = pinImagges[index]

            // ピンを立てる
            pin.coordinate = coordinate
            self.mapView.addAnnotation(pin)
        }
    }

   func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        // 自分の現在地は置き換えない(青いフワフワのマークのままにする)
        if (annotation is MKUserLocation) {
            return nil
        }

        let identifier = "pin"
        var annotationView: MKAnnotationView!

        if annotationView == nil {
            annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        }

        // ピンにセットした画像をつける
        if let pin = annotation as? MapAnnotationSetting {
            if let pinImage = pin.pinImage {
               annotationView.image = pinImage
            }
        }
        annotationView.annotation = annotation
        // ピンをタップした時の吹き出しの表示
        annotationView.canShowCallout = true

        return annotationView
    }


}

これで、カスタムのピンが表示される
S__7512086.jpg

Mapを触ってみて

いろんなことができすぎるのと、仕組みがまだ理解できていないのでちゃんと理解しないとしんどそう・・。
次はFirebaseと連携してUber的なアプリ作る

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

iOS 14で変わるピッカーと、新しく登場したカラーピッカー

WWDC 2020のDesign with iOS pickers, menus and actionsを見たのでメモです。

※ここに出てくるスクリーンショットは、全て上記の動画のものです。

概要

iPhoneやiPadのアプリでiOS 14から使えるようになった3つのコンポーネントについての紹介。
スクリーンショット 2020-06-25 7.52.35.png

Menu

iOS 13まで

iPhoneはアクションシート、iPadの場合はポップオーバーを使ってメニューを表示していた。最近はiPhoneの画面が大きくなったりiPadの画面が高密度になってきたので、次のような欠点が見えてくるようになってきた。

  • 背景が暗くなり、重苦しさがあった
  • アクション名が短くても、シートは大きくなってしまう
  • アクション内容が制限されている
  • ボタンの位置とアクションシートの位置が離れることがあるので指の移動量が多くなることがある

iOS 14 ~

どのボタンからもメニューを表示できるようになった。
スクリーンショット 2020-06-25 7.58.45.png
ボタンをタップしたらすぐそばにメニューが表示される。複数行にも対応してるし、選択やナビゲーションの用途にも使える。
スクリーンショット 2020-06-25 8.05.37.png
表示はコンテキストメニューとよく似ている。各アクションの左側にラベルがあり、右側にはアイコンがついている。アイコンはSF Symbolかカスタム画像を入れることができる。タイトルを追加することも、セパレータを追加することもできる。
スクリーンショット 2020-06-25 8.10.37.png

項目の選択とキャンセル

普通にタップして選択することもできるし、メニューが仕込まれたボタンをタップ、そのまま指を離さず、出てきたメニューの選択したい項目まで指をスライド、指を離してもボタンが選択されたことになる(動画の2:50-3:00くらいを見るといい)。メニューを消すにはメニュー外をタップすれば良い。そのため、キャンセルボタンを用意する必要はない。

特徴

新しいメニューには次のような特徴がある。

曖昧さを解消できる

メニューは選択の曖昧さを解消するのに役立つ。例えば写真アプリでは「+」という追加ボタンがある。これをタップすると具体的に何を追加したいかを尋ねるメニューが出現する。
スクリーンショット 2020-06-25 8.24.02.png
スクリーンショット 2020-06-25 8.24.16.png

ナビゲーションにもなる

メニューはナビゲーションにも使える。例えばSafariでは戻るボタンを長押しするとそのセッションで以前訪れたサイトのリストが表示される。
スクリーンショット 2020-06-25 8.28.29.png

選択ができる

「もっと見る」ボタンもアクションメニューを表示させるうってつけの例。Fileアプリを例に取ると、iOS 13だとアクションがバラバラに配置されていたが、iOS 14だと一つにまとめられる。とはいえ、何でもかんでも「もっと見る」の中に隠すのは推奨してない。メインアクションとサブアクションをしっかり切り分けて考えて配置する必要がある。
スクリーンショット 2020-06-25 8.34.03.png

セカンダリオプションが表示できる

Safariの場合、普通に右上のボタンをタップするとこうなるが…
スクリーンショット 2020-06-25 8.39.42.png
長押しすると関連メニューが出てくるようになっている。
スクリーンショット 2020-06-25 8.40.25.png

破壊的なアクションの扱い

リマインダーアプリで削除を行う場合の例。破壊的なアクションは慎重に確認させるべきものなので、削除の確認アクションはメニューとは別の場所にあるべき。
スクリーンショット 2020-06-25 8.43.04.png
スクリーンショット 2020-06-25 8.45.48.png
これは新メニュー内に限らず、他の破壊的なアクションボタンの場合でも同様。
スクリーンショット 2020-06-25 8.48.18.png

日付ピッカー

iOS 13までの日付、時間ピッカーはこのようなデザインだった。複数のホイールを動かさなければならなかった。
スクリーンショット 2020-06-25 8.53.20.png
iOS 14からはこのようなデザインになる。
スクリーンショット 2020-06-25 8.54.51.png
左右にスライドさせるだけで簡単に月を移動できる。年月の部分をタップするとホイールで選択することもできる。
スクリーンショット 2020-06-25 8.56.44.png
時間は自分で入力することになる。
スクリーンショット 2020-06-25 8.58.26.png
リマインダーの例のようにピッカーをインラインで表示できれば効果的だが、必ずしもそうできない場合もある。その場合はコンパクトモードを使う。UIKitに日付ピッカーをリクエストするとこのようなボタンが表示される。
スクリーンショット 2020-06-25 14.13.19.png
ボタンをタップすると日付ピッカーが表示される。ピッカーの外側をタップすると決定になる。
スクリーンショット 2020-06-25 14.13.58.png

カラーピッカー

iOS 14では新しくカラーピッカーが導入されている。グリッド、スペクトル、RGBから色を選ぶことができる。
スクリーンショット 2020-06-25 14.25.07.png
左上のピペットを使えば拡大鏡が出てくるので、例えば写真から色を選ぶこともできる。
スクリーンショット 2020-06-25 14.26.48.png
また、どのアプリからでもアクセスできるパレットに色を保存することもできる。
スクリーンショット 2020-06-25 14.28.08.png

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

【Swift】一度だけ実行したい処理の書き方

何回も呼ばれるメソッドで1度だけ行いたい処理の記述方法をまとめました。
やり方はたくさんありますが、これが1番私的には使いやすかったです。

1番簡単な方法

class OnceTrackerSample1 {
    var isFirst = Bool

    func manyCalledFunc() {
        if !isFirst { return }
        // 1回のみ行いたい処理 
    }
}

1番最初に全ての人間が思いつきそうな処理。
やっぱりダサいので、使い勝手よくカッコよく書きたい。

DispatchQueueを拡張して使いやすくする

DispatchQueueextensionします。

private static var _onceTracker = [String]()

class func once(token: String, block:()->Void) {
    // 排他制御
    objc_sync_enter(self); defer { objc_sync_exit(self) }
    // token: Stringで一意に実行状況を管理しています
    if _onceTracker.contains(token) { return }

    // 実行履歴を残して、block:()->Voidで渡された処理を実行
    _onceTracker.append(token)
    block()
}

使う側はこんな感じです。

class OnceTrackerSample1 {
    func manyCalledFunc() {
        DispatchQueue.once(token: "key name") {
            // 1回のみ行いたい処理 
        }
    }
}

スマートでいい感じ!
Stringkeyはどこかでまとめて管理したらより使いやすそう。

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

swiftメモ

コードから直接セーフアリアにconstraintする

swift5
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let webView = WKWebView(frame: .zero)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        view.addSubview(webView)

        webView.leftAnchor.constraint(equalTo: view.safeLeftAnchor).isActive = true
        webView.rightAnchor.constraint(equalTo: view.safeRightAnchor).isActive = true
        webView.topAnchor.constraint(equalTo: view.safeTopAnchor, constant: 60).isActive = true
        webView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: 100).isActive = true
    }

}

配列を値から検索して削除

削除したい値以外で新しい配列を作成

swift5
        var array = ["a","b","c","d","e","f"]
        array = array.filter { $0 != "d" }
        print(array)    //結果:["a", "b", "c", "e", "f"]

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

iOS Add the following keys to your Info.plist file, located in <project root>/ios/Runner/Info.plist:のエラーハンドリング

エラー内容

iOS Add the following keys to your Info.plist file, located in /ios/Runner/Info.plist: NSPhotoLibraryUsageDescription - describe why your app needs permission for the photo library. This is called Privacy - Photo Library Usage Description in the visual editor. NSCameraUsageDescription - describe why your app needs access to the camera. This is called Privacy - Camera Usage Description in the visual editor.

Flutterアプリケーション内でカメラへのアクセスと写真ライブラリーを試みた時に生じたエラー。アクセスを試みると同時にアプリが落ちてしまう。

Info.plistファイルに許可を与えることが必要らしい。

変更

スクリーンショット 2020-06-25 10.53.55.png
xcodeを開き、Info.plistファイルへ。

画像を見て一番上の二つのpropertyを加える。

  1. key: Privacy - Camera Usage DescriptionPrivacy - Photo Library Usage Descriptionを加える。

  2. 型を決め、value:NSCameraUsageDescriptionNSPhotoLibraryUsageDescriptionを加えれば、カメラへのアクセス、写真ライブラリーへのアクセスが可能になる。

方法

加え方は簡単でkeyのどれかにカーソルを合わせると+ボタンが出てくるのでそれをタップするとkeyの一覧が自動的に出てくる。

あとは追加したいkey、型、Valueを一覧から選ぶだけ。

参考

日本語情報が全然出てこなかったので書きました。下記のStackoverflowのリンクでより丁寧に方法を教えてくれているので英語で良いという方はこちらの方を参考にしてください。

NSPhotoLibraryUsageDescription key must be present in Info.plist to use camera roll

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

UIButtonを画像付きのテキストにする

テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImageUILabelを組み合わせなくてもUIButtonのみで作ることができます。

  • R.swiftを使っています
  • 表示する画像はこちらのイカちゃん iks.png

完成はこんな感じです。

UIButtonに画像とテキストを設定する

leftImageButton.setTitle("左側アイコン", for: .normal)
leftImageButton.setImage(R.image.ika(), for: .normal)

titleEdgeInsets・imageEdgeInsetsを使います

EdgeInsetsは余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsetsimageEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)なので、画像とテキストを同時に設定すると被って表示されます。

テキストの左に画像を付ける

テキストの左側に余白を設定します。画像のwidthを元に設定するのが良いと思います。

leftImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)

テキストの右に画像を付ける

画像を右側に持っていく為に左側にテキストの長さ分の余白を設定します。こちらも固定値を設定していますが、テキストの長さから設定した方が良いと思います。
またテキストの位置も調整したいので、テキストの余白も設定します。

rightImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 200, bottom: 0, right: 0)
rightImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)

仕組みがわかれば同じ要領でテキストの上側に画像を設定したり、下に画像を設定したりできます。

テキストの上に画像を付ける

topImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 100, bottom: 20, right: 0)
topImageButton.titleEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)

テキストの下に画像を付ける

bottomImageButton.imageEdgeInsets = UIEdgeInsets(top: 25, left: 100, bottom: 0, right: 0)
bottomImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)

以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。

関連

【Swift】UIButtonにimageをセットしたが表示されない件

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

【Swift】UIButtonを画像付きのテキストにする

テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImageUILabelを組み合わせなくてもUIButtonのみで作ることができます。

  • R.swiftを使っています
  • 表示する画像はこちらのイカちゃん

iks.png

完成はこんな感じです。

UIButtonに画像とテキストを設定する

leftImageButton.setTitle("左側アイコン", for: .normal)
leftImageButton.setImage(R.image.ika(), for: .normal)

titleEdgeInsets・imageEdgeInsetsを使います

EdgeInsetsは余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsetsimageEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)なので、画像とテキストを同時に設定すると被って表示されます。

テキストの左に画像を付ける

テキストの左側に余白を設定します。画像のwidthを元に設定するのが良いと思います。

leftImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)

テキストの右に画像を付ける

画像を右側に持っていく為に左側にテキストの長さ分の余白を設定します。こちらも固定値を設定していますが、テキストの長さから設定した方が良いと思います。
またテキストの位置も調整したいので、テキストの余白も設定します。

rightImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 200, bottom: 0, right: 0)
rightImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)

仕組みがわかれば同じ要領でテキストの上側に画像を設定したり、下に画像を設定したりできます。

テキストの上に画像を付ける

topImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 100, bottom: 20, right: 0)
topImageButton.titleEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)

テキストの下に画像を付ける

bottomImageButton.imageEdgeInsets = UIEdgeInsets(top: 25, left: 100, bottom: 0, right: 0)
bottomImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)

以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。

関連

【Swift】UIButtonにimageをセットしたが表示されない件

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

SwiftUIでナビゲーションバーの中央にロゴ画像を配置する方法

最近SwiftUIを触り始めて、巷でよく見るNavigationBarの中央にロゴ画像を配置する実装をしようとしたところ、少しつまづいたので、備忘録がわりに書きます。

最終的にSwiftUIで以下の画像の様な状態を目指します。
スクリーンショット 2020-06-24 20.40.34.png

UIKit

まずは、UIKitでナビゲーションバーで中央に画像を配置する場合はUIViewControllerのnavigationItemが持つtitleViewというプロパティにUIImageViewを設定することで、表示することができます。

import UIKit

class ViewController: UIViewController {

    func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.titleView = UIImageView(image: UIImage(named: "logo"))
    }
}

SwiftUI

SwiftUIで同じことをやろうとした場合、NavigationViewのタイトルの文字列を設定する関数は存在するのですが、どうやらUIKitでのtitleViewにあたる、ナビゲーションバーの中央にViewを配置する関数というのは存在しない様です。

import SwiftUI 

struct SwiftUIView: View {

    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarTitle("App Name", displayMode: .inline) 
                // 文字列は設定できるが、Imageは設定できない?
        }
    }
}

スクリーンショット 2020-06-24 21.02.33.png

調べてみるとナビゲーションバー左右にViewを配置する関数を利用して、画像を配置してから、画面のサイズを元に中央に寄せる方法などもあったのですが、元来想定されていなさそうな使い方の様に感じたので、別の方法で試してみました。

それが以下の方法です。

import SwiftUI

struct SwiftUIView: View {

    var body: some View {
        ZStack(alignment: .top) {
            NavigationView {
                Text("Hello, World!")
                    .navigationBarTitle("", displayMode: .inline)
            }
            Image(uiImage: UIImage(named: "logo"))
                .resizable()
                .scaledToFit()
                .frame(width: nil, height: 44, alignment: .center)
        }
    }
}

alignmentを.topに設定したZStack(Viewの重なりを設定するStack)に、NavigationViewとImageを配置することで、最前面の上部であるナビゲーションバーの中央にImageを表示する仕組みです。

この様にすると無事、UIKitと同様にNavigationBarの上部にロゴが表示されました
スクリーンショット 2020-06-24 21.12.40.png

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

EmbeddedFrameworkでCarthageを使用した際にPreviewがPotentialCrashError: Update failedになる場合の対処法

Xcode11から使用できる様になったPreview機能はSwiftUIでの開発をサポートしてくれる強力な味方ではあるのですが、特定の状況で読み込めなくなってしまうこともあり、そうなった場合は開発が滞ったりしてしまいます。

今回、EmbeddedFrameworkとCarthageを利用してSwiftUI開発している際に、Previewが読み込めなくなってハマった際の対処法を紹介します。

環境

  • Swift5.2
  • Xcode11.5
  • Carthage
    • Nuke(画像キャッシュ)
    • Alamofire(API通信)

プロジェクトの構成はアプリのターゲットに、DomainDataStorePresentationの三つのEmbeddedFrameworkを追加しています。
スクリーンショット 2020-06-25 0.21.05.png

UI周りのコードはPresentationに全て書いているので、そのFramework内でPreviewを確認します。

PotentialCrashError: Update failed

では、早速PresentationのFramework内に試しに書いたPreviewを見てみましょう。

struct SwiftUIView_Previews: PreviewProvider {

    static var previews: some View {
        Text("Hello, World!")
    }
}

スクリーンショット 2020-06-25 0.28.47.png

読み込めない?

エラーの詳細を見てみると・・・

スクリーンショット 2020-06-25 0.29.09.png

どうやらCarthageで導入しているNukeが読み込めないがためにPreviewが表示されないようです。

スクリーンショット 2020-06-25 0.37.05.png

しかしシミュレーターのビルドは通っているので、ちゃんとCarthageで導入したFrameworkの参照はうまくいってるはず?

解決法

色々と調べていると、PreviewからFrameworkの参照するためのパスの設定はBuild SettingのFramework Search PathsではなくRunpath Search Pathsから行われているようです。

スクリーンショット 2020-06-25 0.48.01.png

そうと分かれば、以下の画像のようにRunpath Search Pathに、新たにFramework Search Pathsに入力されている$(PROJECT_DIR)/Carthage/Build/iOSを追加してあげれば、Preview時にCarthageで導入したFrameworkも参照できるようになりそうです。

スクリーンショット 2020-06-25 0.51.33.png

この状態でPreviewを確認すると・・・

スクリーンショット 2020-06-25 0.56.24.png

ちゃんと表示された!?

最後に

以上がEmbeddedFrameworkにCarthageで導入したFramework際にPreviewが表示されない場合の対処法でした。

この記事によって、誰かが私のようにこの問題を修正するために、EmbeddedFramework化を諦めそうになったり、休日の大事な時間を無駄にしたりしないことを祈ります?

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

iOSで100vhが効かない問題で、楽な方法をNuxt.jsでやってみる

iOS safariで100vhがうまくいかない時に、
window.innerHeightを取って各要素のstyle属性に一個一個付ける、
というのをやりがちでしたが、楽な方法が無いかと探すとカスタムプロパティが使えるそうなので、
その手順をNuxt.js版で書きます。

mixinを作る

--whという名前でカスタムプロパティを定義する。
ロード時と、リサイズ時にwindow.innerHeightを取って--whに入れる。

mixins/device.js
export default {
  data: () => ({
    style: {
      '--wh': '100vh'
    }
  }),
  mounted() {
    this.$nextTick(() => {
      this.getWindowSize()
      window.addEventListener('resize', this.getWindowSize)
    })
  },
  methods: {
    getWindowSize() {
      this.style['--wh'] = `${window.innerHeight}px`
    }
  }
}

mixinを読み込む

mixinを読み込んで、styleオブジェクトをwrapper要素のstyle属性に入れる。

layouts/default.vue
<template lang="pug">
.l-wrap(:style="style")
  nuxt
</template>

<script>
import deviceMixin from '~/mixins/device'

export default {
  mixins: [deviceMixin]
}
</script>

カスタムプロパティを使う

heightにvar(--wh, 100vh)と書いて、使用する。
書き方: var(設定したカスタムプロパティ名, プロパティが無い時用の初期値)

pages/index.vue
<template lang="pug">
.p-top
</template>

<script>
export default {}
</script>

<style lang="stylus" scoped>
.p-top
  height var(--wh, 100vh)
</style>

まとめ

これで、iOSのツールバーを考慮した実装が

  1. wrapperでwindow.innerHeight取って、変数に格納する
  2. wrapperかそれ以下の小要素でheight var(--wh, 100vh)する

という形でほぼCSSで指定出来るようになりました。

IEはまだ未検証ですが、Nuxt.jsの場合はheight var(--wh, 100vh)とすると、
height 100vhを自動的に手前に追加してくれるようなので、
varが認識されなくても問題なくvhできるかと思います。

<style lang="stylus" scoped>
.p-top
  height 100vh
  height var(--wh, 100vh)
</style>

※mixinを使ったのはlayoutsが複数あるときに、各所に同じ処理を入れることになりそうで面倒だと思ったからなので、default.vueでmixinでやってる処理をそのまま書いても問題ないです。

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