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

XLPagerTabStripが使われている画面でスワイプバックを使いたい

問題

NavigationControllerで遷移した先の画面がXLPagerStripを使用していた場合、配置されてるcontainerView(UIScrollView)に引っ張られて
スワイプバック(Interactive Pop Gesture)が効かなくなってしまう。

解決策

以下のコードをviewWillAppearに追加する。

containerViewのGestureはNavigationControllerのGestureが失敗した時に実行されるようにする?という感じ。

if let naviVc = self.navigationController {
    self.containerView.panGestureRecognizer.require(toFail: naviVc.interactivePopGestureRecognizer!)
}

参考サイト

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

[Swift, ARKit]検出した平面を使って動画をマルチスクリーンで再生する

SwiftのARKitには平面を検出するという機能があります。
一般的な使い方としては検出した平面を色付けしたりタップして他の3Dモデルを置いたりといったものだと思いますが、今回はその平面をそのままテレビのように動画を再生するスクリーンにしてみました。

流れとしては
平面を検知してARPlaneAnchorを置く

ARPlaneAnchorが置かれた時にSCNPlaneを作成する

SCNPlaneに動画を貼り付けて再生する

SCNPlaneをもとにSCNNodeを作成する
というものです。
自分でARPlaneやARBoxを作成して動画を貼り付けるものはいくつか見つかりましたが、平面を検出して動的にARPlaneを増やし続けるものは無かったのでやってみようと思います。

検証環境

  • Swift 5.3
  • Xcode 12.0
  • iOS 14.0.1

再生する動画は以下のものを使用しました。以前会社で作成したものです。
ちょっと長いので78秒からの失敗例を出した部分だけ切り取りました。
https://youtu.be/4xfmPCFUplU?t=78

結果

ソースコードが長いので最初に結果から。
こういうものが作れます。
result.gif
結果から言えば上手くいきました。
テーブルやイス、壁などを平面として検出してその度に動画を貼り付けています。
後述しますが大きさが条件を満たしていれば正方形にも縦長にも変えられるようです。
少し分かりづらいですが各動画が独立して再生されていることも確認できます。
ちなみに動画なので音声も流れます。かつ各動画が独立しているので複数の音声が異なるタイミングで飛んできます。
作った自分が言うのもなんですが洗脳色が強いです。

ソースコード

全体のソースコードは以下の通りです。
大まかな説明はコメントアウトしています。

ViewController.swift
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    var videoURL: URL!

    override var prefersStatusBarHidden: Bool { return true }
    override var prefersHomeIndicatorAutoHidden: Bool { return true }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.scnView.scene = SCNScene()
        self.scnView.delegate = self
        //再生する動画のURLを取得
        self.videoURL = Bundle.main.url(forResource: "pien", withExtension: "mp4")!
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let configuration = ARWorldTrackingConfiguration()
        //平面検出を水平・垂直両方に設定
        configuration.planeDetection = [.vertical, .horizontal]
        configuration.isLightEstimationEnabled = true
        self.scnView.session.run(configuration)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.scnView.session.pause()
    }
    //動画を再生するためのスクリーン作成部分
    func createVideo(size: CGSize) -> SKScene {
        let skScene = SKScene(size: CGSize(width: 1280, height: 700))
        let player = AVPlayer(url: videoURL)
        //動画をリピート設定に
        NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
                                               object: player.currentItem, queue: nil) { (_) in
            player.seek(to: CMTime.zero)
            player.play()
        }

        let skNode = SKVideoNode(avPlayer: player)
        //動画の位置と向きを調整
        skNode.position = CGPoint(x: skScene.size.width / 2.0, y: skScene.size.height / 2.0)
        skNode.size = skScene.size
        skNode.yScale = -1.0
        skNode.play()
        skScene.addChild(skNode)

        return skScene
    }

    //平面を検出してARAnchorが置かれた時に発火するDelegate Method
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else {
            return
        }
        DispatchQueue.main.async {
            //動画を再生するスクリーンとなるPlaneを作成
            let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
            let planeNode = SCNNode(geometry: plane)
            //Planeの位置と向きを調整
            planeNode.position = SCNVector3(x: planeAnchor.center.x,
                                            y: planeAnchor.center.y,
                                            z: planeAnchor.center.z)
            planeNode.eulerAngles.x = -.pi / 2
            node.addChildNode(planeNode)
        }
    }

    //既に存在するARAnchorが更新された時に発火するDelegate Method
    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor,
              let plane = node.childNodes.first?.geometry as? SCNPlane else {
            return
        }
        //Planeの大きさが20cm x 20cm以上かどうかで分岐 この条件は自由
        if plane.height > 0.2 && plane.width > 0.2 {
            //既にplaneに動画が貼り付けられている場合は処理を抜ける
            if plane.materials.first?.diffuse.contents is SKScene {
                return
            }
            let size = CGSize(width: plane.width, height: plane.height)
            let skScene = self.createVideo(size: size)
            let material = SCNMaterial()
            material.diffuse.contents = skScene
            plane.materials = [material]
        }else {
            plane.width = CGFloat(planeAnchor.extent.x)
            plane.height = CGFloat(planeAnchor.extent.z)
        }
    }
}

個別解説

self.videoURL = Bundle.main.url(forResource: "pien", withExtension: "mp4")!

let player = AVPlayer(url: videoURL)

まず動画の読み込みについてですが、URLはviewDidLoad()で、AVPlayerはcreateVideo()というメソッドで取得しています。
AVPlayerをURLと同じviewDidLoad()で取得してメンバ変数として残しておくこともできますが、その場合は各スクリーンで再生される動画が全て同じものとなります。

let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
let planeNode = SCNNode(geometry: plane)
planeNode.position = SCNVector3(x: planeAnchor.center.x,
                                y: planeAnchor.center.y,
                                z: planeAnchor.center.z)
planeNode.eulerAngles.x = -.pi / 2

次にスクリーンとなるPlaneを作成する部分。
気をつけないといけないのはSCNPlaneの引数heightにARPlaneAnchorのextent.zを指定していることです。
ARPlaneAnchorは奥行きがy軸となっているので、引数にextent.yを指定すると表示されません。
また、作成したplaneNodeの向きeulerAngles.xもデフォルトだと検出した平面に対して垂直になっているので、平面の上にかぶせる形に修正が必要です。

if plane.height > 0.2 && plane.width > 0.2 {
    if plane.materials.first?.diffuse.contents is SKScene {
        return
    }
    let skScene = self.createVideo()
    let material = SCNMaterial()
    material.diffuse.contents = skScene
    plane.materials = [material]
}else {
    plane.width = CGFloat(planeAnchor.extent.x)
    plane.height = CGFloat(planeAnchor.extent.z)
}

最後に動画をSCNPlaneに貼り付ける部分です。
今回は条件式でplaneの高さと幅が一定以上の値かどうかをチェックしていますが、これは条件を設定しない場合、少しでもARPlaneAnchorが更新されるとその時点で動画を貼り付けてしまうためです。
一応動画を貼り付けた後でも高さや幅は更新できますが、その度に動画のサイズを変更して改めて貼り付けることになる上、動画も最初から再生されることになります。
高さや幅などの条件は自由に変えることができます。
条件式がTrueの場合に改めて条件式が出てきますが、これは既に動画を貼り付けている場合に再度貼り付けることを防ぐものです。

条件式がFalseの場合には更新されたARPlaneAnchorの大きさを既存のSCNPlaneに適用しています。
こちらもPlaneを作成した部分と同様、extent.yを指定すると表示されなくなるので注意してください。

まとめ

結果は最初にお見せした通りです。平面を検出し、マルチスクリーンで動画を再生することができました。
今回再生した動画は1つだけですが、条件を変えることで複数種類の動画を各スクリーンで再生することができると思います。
ここまで読んでいただきありがとうございました。

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

Processing(Java)からSwiftで書いたプログラムを呼び出す

Processing を使ってユーティリティ系のソフトを作っていると、どうしてもかゆいところに手がとどかない(システム環境設定系とか)

Swiftで実装すればAPIを叩くのに苦労はしないが、
Java側で Runtime#exec とか ProcessBuilder を使うのはなんか違う気がするので、
どうにかしてして、Javaで実装されたメソッドのようにSwiftで実装した関数を叩きたい!

JNA(Java Native Access)

Processingの実体はPAppletクラスを継承したJavaクラスであるため、
Javaの文法、資産を利用することができる

Javaには、JNI(Java Native Interface)と呼ばれる、C/C++などで実装されたネイティブコードと連携する仕組みがある。
このJNIを利用すれば、Swiftで実装したネイティブコードを呼び出されるのではと思うが、
このJavaとC/C++を繋ぐJNIを書くのがマヂ無理
(パッケージ名を含めた関数名をつけた関数をつくったり...)

※ Swiftの実装を呼び出すには結局SwiftとJavaの間にCのコードを挟まないといけない
Write & call Swift code using Java’s JNI
https://stackoverflow.com/questions/27628385/write-call-swift-code-using-java-s-jni

あー、Javaからネイティブコードを呼びたい
けど、JNIは書きたくない

そんなときは JNA(Java Native Access) を使おう
なんかよしなにやってくれるらしい!

JNAに関してはこのあたりを参照されたし
https://qiita.com/everylittle/items/b888cbec643f14de5ea6

Swiftのソースからダイナミックリンクライブラリを生成

兎にも角にも、Javaから呼び出したいメソッドを作ります

hello.swift
import Darwin.C

@_cdecl("hello")
public func hello() {
    print("Hello")
    fflush(stdout)
}

上のプログラムは標準出力に Hello と表示する関数の実装
ポイントは 1.呼び出し規約を設定する,2.publicにする です

fflush(stdout) がないとprintされないらしいので入れておきましょう
Printf from native code appears only at the end of the java app
https://stackoverrun.com/ja/q/3379588

このままではソースのままなのでコンパイルしましょう
上のプログラムからダイナミックリンクライブラリ(MacOSだとlib*.dylib)を生成します

ダイナミックリンクライブラリを生成
swiftc -emit-library hello.swift

ターミナルで上のコマンドを実行すると lib + ファイル名(拡張子なし) + .dylib
ダイナミックリンクライブラリが生成されます
image.png

nmコマンドで確認すると hello関数がグローバルセクションにあってなんか呼べそうな気がしてきますね

nmコマンドで確認
cha84rakanal$ nm libhello.dylib 
0000000000000e40 T _$S5helloAAyyF
                 U _$S6Darwin6stdoutSpySo7__sFILEVGvg
                 U _$SSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
                 U _$SSSN
                 U _$Ss27_allocateUninitializedArrayySayxG_BptBwlFyp_Tg5
                 U _$Ss5print_9separator10terminatoryypd_S2StF
                 U _$Ss5print_9separator10terminatoryypd_S2StFfA0_
                 U _$Ss5print_9separator10terminatoryypd_S2StFfA1_
0000000000000fac s ___swift_reflection_version
                 U __swift_FORCE_LOAD_$_swiftDarwin
0000000000001060 s __swift_FORCE_LOAD_$_swiftDarwin_$_hello
                 U _fflush
0000000000000e30 T _hello
                 U _swift_bridgeObjectRelease
                 U _swift_bridgeObjectRetain
                 U dyld_stub_binder
cha84rakanal$ 

実際にJava側からSwiftで実装した関数を呼び出す

さて実際にProcessing(Java)側から先程作った Hello関数 を呼び出してみます
スケッチのdataディレクトリに先に生成したダイナミックリンクライブラリを入れて、次のスケッチで動かします!

JNAProcessing.pde
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

public interface SwiftLib extends Library {
    void hello();
}

SwiftLib mylib;

void setup(){
    mylib = (SwiftLib) Native.loadLibrary(dataPath("libhello.dylib"), SwiftLib.class);
    mylib.hello();
}

void draw(){
}

ポイントは、

  1. JNA関連のライブラリをインポート
  2. com.sun.jna.Libraryを継承したインターフェースを定義(中に作成した関数の定義)
  3. Native#loadLibrary でダイナミックリンクライブラリを読み込み、ライブラリ名だけだと、Javaが参照しているダイナミックライブラリのパスをみにいく、絶対パスで入れられると確実

これでHelloと表示されたらOK! あとは何でも好きな処理をSwiftで実装するだけ!
それでは、よいProcessing Life

image.png

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

[SwiftUI]backgroundの指定場所ごとにどのように変わるかを調べてみた

SwiftUIでbackgroundの指定で思ったように塗りつぶしてくれなかったりしたので、どこで指定したらどのように塗り潰されるかを調べてみました。

コードとView

struct ContentView: View {

    var body: some View {
        VStack {
            Spacer()
            Text("Hello world!")
                .foregroundColor(Color.white)
                .background(Color.green)
                .frame(maxWidth: .infinity, minHeight: 50)
                .background(Color.blue)
                .padding()
                .background(Color.orange)
            Spacer()
        }.frame(width: 320, height: 100)
    }

}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())

スクリーンショット 2020-10-06 11.30.38.png

解説

  • frameの前にbackgroundを指定すると、テキストギリギリの範囲で塗りつぶされました(緑色)
  • frameの後にbackgroundがあり、かつbackgroundの後にpaddingがある場合は、padding分内側で塗りつぶされました(青)
  • paddingの後にbackgroundを指定すると、paddingも無視した領域で塗りつぶされました(オレンジ)

このようにbackgroundの位置に注意することで、想定通りの塗りつぶしができそうです。

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

デバイスのライトを点ける

フラッシュや懐中電灯に使われているライトを使う方法です。
点灯消灯を制御できます。

Oct-06-2020 10-21-18.gif

AVFoundationのAVCaptureDeviceから制御できます。

1,点灯

let avCaptureDevice = AVCaptureDevice.default(for: AVMediaType.video)

if avCaptureDevice!.hasTorch, avCaptureDevice!.isTorchAvailable { // キャプチャデバイスにライトがあるか、 ライトが使用可能な状態か
    do {
        try avCaptureDevice!.lockForConfiguration() // デバイスにアクセスするときはこれする。
        try avCaptureDevice!.setTorchModeOn(level: 1.0) // 点灯。明るさレベルは 0.0 ~ 1.0
    } catch let error {
        print(error)
    }
    avCaptureDevice!.unlockForConfiguration()
}

torchMode = .on でも最大レベルで点灯できます。
torchMode = .auto は「キャプチャデバイスは継続的に光のレベルを監視し、必要に応じてトーチを使用します。」とのことです。

2,消灯

do {
    try avCaptureDevice!.lockForConfiguration()
} catch let error {
    print(error)
}
avCaptureDevice!.torchMode = .off
avCaptureDevice!.unlockForConfiguration()

?


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

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

リーダブルコードで読みにくいコードを改善していく ~ 全般編

どうも、ねこきち(@nekokichi1_yos2)です。

リーダブルコード改善シリーズ、第4弾、です。

(詳しくは下記をご参照ください)
リーダブルコードで読みにくいコードを改善していく ~ 準備編
リーダブルコードで読みにくいコードを改善していく ~ 名前編
リーダブルコードで読みにくいコードを改善していく ~ 美しさ編
リーダブルコードで読みにくいコードを改善していく ~ コメント編

今回は、コード全体を改善していきます。

リーダブルコードの教え

「条件式を揃える」
・条件は肯定形を使う
・分かりやすい条件を先に書く

「三項演算子は何も凄くない」
・コードを短くできるが、読みにくい

「関数は早めに返す」
・出口を複数用意する
・コードが冗長にならずに済む

「ネストは簡潔でシンプルに」
・ネストが深くなれば、読み手に負担がかかる
・条件式や変数を常に覚える必要があり、ネスト内部の理解に集中力を要する

「説明変数、要約変数」
・説明変数:変数名でどんな値かを示す
・要約変数:巨大な値や式を変数でまとめる

「短絡評価の悪用」
・単に少ないコードが良いわけではない
・同じ内容でも、理解しやすい方が望ましい

「巨大な式を分割する」
・同じ式が何度も登場する場合、要約変数を使用
・メリット:タイプミスの現象、コードが短くなる、修正しやすくなる

「変数を削除」
・変数を使う必要があるか?を考慮する
・変数に代入しなくても、楽に理解できるなら必要なし

「変数のスコープを縮める」
・スコープが広いと、どこで変更されたか追跡が難しくなる
・グローバル→ローカル、にスコープを縮めよ
・アクセス修飾子(private,public,static)を使用する

「定義の位置を下げる」
・定義と処理に区切ると、常に変数の値を把握する必要がある
・定義と処理をセットにすれば、必要な変数を見つけられ、読みやすくなる

「変数は一度だけ書き込む」
・変更される回数が多いと、現在値がわからなくなる
・letやprivateを使用し、永続的な変更を禁止にする
・説明変数などで変更される回数を減らす

巨大な式を分割する

memoTextViewのattributedTextをData()に変換していますが、UIの値をそのまま代入するのは危なく、プロパティ名が長いので、guardlet文での変数inputAttrTextに入れました。

AddMemo.swift
let attributedMemoData = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false)

AddMemo.swift
guard let inputAttrText = memoTextView.attributedText else { return }
let attributedMemoData = try! NSKeyedArchiver.archivedData(withRootObject: inputAttrText, requiringSecureCoding: false)

prepare()内で遷移先の変数に値を代入しています。

memoTableViewの選択されたrow番目の値を参照してますが、プロパティ名が長すぎて見づらいので、変数indexPathRowを用意し、代入する式の長さを短くしました。

ViewController.swift
let vc = segue.destination as! DisplayMemo
vc.selectedMemoObject   = memoListForRealm[memoTableView.indexPathForSelectedRow!.row]
vc.selectedMemoString   = memoList[memoTableView.indexPathForSelectedRow!.row]
vc.selectedIndexPathRow = memoTableView.indexPathForSelectedRow!.row

ViewController.swift
let vc = segue.destination as! DisplayMemo
guard let indexPathRow = memoTableView.indexPathForSelectedRow?.row else {
    return
}
vc.selectedMemoObject   = memoListForRealm[indexPathRow]
vc.selectedMemoString   = memoList[indexPathRow]
vc.selectedIndexPathRow = indexPathRow

変数と読みやすさ

不要な変数を削除

変数memoObjectのプロパティに値を代入しています。

が、Data()に変換したinputAttrTextを変数attributedMemoDataに代入してますが、わざわざ変数を介して渡すより、直接渡した方がコードを1文減らせるので、変数attributedMemoDataを削除しました。

AddMemo.swift
@IBAction func addMemo(_ sender: Any) {
    guard let inputAttrText = memoTextView.attributedText else { return }
    let memoObject             = MemoModel()
    // memoTextView.attributedText -> Data()
    let attributedMemoData     = try! NSKeyedArchiver.archivedData(withRootObject: inputAttrText, requiringSecureCoding: false)
    memoObject.data            = attributedMemoData
    memoObject.identifier      = String().randomString()

AddMemo.swift
@IBAction func addMemo(_ sender: Any) {
    guard let inputAttrText = memoTextView.attributedText else { return }
    let memoObject             = MemoModel()
    // memoTextView.attributedText -> Data()
    memoObject.data            = try! NSKeyedArchiver.archivedData(withRootObject: inputAttrText, requiringSecureCoding: false)
    memoObject.identifier      = String().randomString()

セルをスワイプで削除した時、RealmとtableViewに使用する配列のそれぞれの値を削除してます。

しかし、RealmはResults<モデル名>に指定した変数を操作すれば、その変更が反映されるので、無駄な処理を消しました。

ViewController.swift
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    let selectedMemo = memoListForRealm[indexPath.row]

    //Realm-Delete
    try! realm.write() {
        realm.delete(selectedMemo)
        realm.delete(memoListForRealm[indexPath.row])
    }

ViewController.swift
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

    //Realm-Delete
    try! realm.write() {
        realm.delete(memoListForRealm[indexPath.row])
    }

変数のスコープを縮める

privateは、参照(get)と変更(set)を制限するアクセス修飾子です。

privateを付与することで、他ファイルや関数から利用されるのを防ぎ、他ファイルでの変更や参照を気にすることなくなります。

また、privateで
(この変数は他ファイルでは使われない)
と明示的に示すことができ、コードの追跡する手間が無くなります。

なので、可能な限り、全ての変数と関数にprivateをつけました。

@IBActionにまでprivateをつける必要はありませんが、修飾子が無いと、どこかで実行されて無いかと思うので、敢えてつけました。

ViewController.swift
@IBOutlet weak var memoTableView: UITableView!

// Realmへ保存用のメモリスト
var memoListForRealm:Results<MemoModel>!
// Realm
let realm               = try! Realm()
// メモリスト
var memoList            = [NSAttributedString]()

ViewController.swift
@IBOutlet private weak var memoTableView: UITableView!

// Realmへ保存用のメモリスト
private var memoListForRealm:Results<MemoModel>!
// Realm
private let realm               = try! Realm()
// メモリスト
private var memoList            = [NSAttributedString]()

AddMemo.swift
let realm       = try! Realm()
let imagePicker = UIImagePickerController()

@IBAction func addMemo(_ sender: Any) {

@IBAction func attachImageGesture(_ sender: UILongPressGestureRecognizer) {

AddMemo.swift
private let realm       = try! Realm()
private let imagePicker = UIImagePickerController()

@IBAction private func addMemo(_ sender: Any) {

@IBAction private func attachImageGesture(_ sender: UILongPressGestureRecognizer) {

EditMemo.swift
@IBOutlet weak var memoTextView: UITextView!

// Realm
let realm                       = try! Realm()
let imagePicker                 = UIImagePickerController()

@IBAction func attachImageGesture(_ sender: UILongPressGestureRecognizer) {

@IBAction func updateMemo(_ sender: Any) {

EditMemo.swift
@IBOutlet private weak var memoTextView: UITextView!

// Realm
private let realm                       = try! Realm()
private let imagePicker                 = UIImagePickerController()

@IBAction private func attachImageGesture(_ sender: UILongPressGestureRecognizer) {

@IBAction private func updateMemo(_ sender: Any) {

DisplayMemo.swift
@IBOutlet weak var memoTextView: UITextView!

DisplayMemo.swift
@IBOutlet private weak var memoTextView: UITextView!

finalは継承とオーバーライドを制限する修飾子です。

明示的に、このクラスは継承もオーバーライドもできません、と示します。

class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
class AddMemo: UIViewController {
class DisplayMemo: UIViewController {
class EditMemo: UIViewController {

final class ViewController:UIViewController,UITableViewDelegate,UITableViewDataSource {
final class AddMemo: UIViewController {
final class EditMemo: UIViewController {
final class DisplayMemo: UIViewController {

定義の位置を下げる

変数宣言と関数の実行を分けると、見やすくなりますが、各変数の存在を覚えておかないと、処理の順序がわからなくなります。

そこで、変数を定義する位置を下げることで、実行される処理の近くに必要とされる変数が定義されてるので、理解度が高まります。

下のコードでは、変数の定義、関数の処理、それぞれのコードが多いので分けています。

ですが、定義されている変数が多く、一度に全部を覚えながら、後々の処理を理解するのは困難でした。

そこで、関連する変数と処理をセットにして、整理しました。

ViewController.swift
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    if let pickerImage = info[.originalImage] as? UIImage {
        // NSAttributedStringへ変換に必要なパラメーター
        let width                 = pickerImage.size.width
        let padding               = self.view.frame.width / 2
        let scaleRate             = width / (memoTextView.frame.size.width - padding)
        // 10%に圧縮した画像
        let resizedImage          = pickerImage.resizeImage(withPercentage: 0.1)!
        let imageAttachment       = NSTextAttachment()
        var imageAttributedString = NSAttributedString()
        // memoTextView.attributedText -> NSMutableAttributedString
        let mutAttrMemoText       = NSMutableAttributedString(attributedString: memoTextView.attributedText)

        // resizedImage -> NSAttributedString()
        imageAttachment.image = UIImage(cgImage: resizedImage.cgImage!, scale: scaleRate, orientation: resizedImage.imageOrientation)
        imageAttributedString = NSAttributedString(attachment: imageAttachment)
        mutAttrMemoText.append(imageAttributedString)
        // 画像を追加後のテキスト -> memoTextView.attributedText
        memoTextView.attributedText = mutAttrMemoText
    }
    dismiss(animated: true, completion: nil)
}

ViewController.swift
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    if let pickerImage = info[.originalImage] as? UIImage {
        let width                 = pickerImage.size.width
        let padding               = self.view.frame.width / 2
        let scaleRate             = width / (memoTextView.frame.size.width - padding)
        // 10%に圧縮した画像
        let resizedImage          = pickerImage.resizeImage(withPercentage: 0.1)!
        let imageAttachment       = NSTextAttachment()
        // resizedImage -> NSAttributedString()
        imageAttachment.image = UIImage(cgImage: resizedImage.cgImage!, scale: scaleRate, orientation: resizedImage.imageOrientation)

        var imageAttributedString = NSAttributedString()
        imageAttributedString = NSAttributedString(attachment: imageAttachment)

        // memoTextView.attributedText -> NSMutableAttributedString()
        let mutAttrMemoString     = NSMutableAttributedString(attributedString: memoTextView.attributedText)
        mutAttrMemoString.append(imageAttributedString)
        // 画像を追加後のテキスト -> memoTextView.attributedText
        memoTextView.attributedText = mutAttrMemoString
    }
    dismiss(animated: true, completion: nil)
}

UIAlertControllerに使用する、2つのUIAlertActionの定義と実装をセットにしました。

@IBAction private func attachImageGesture(_ sender: UILongPressGestureRecognizer) {
    let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet)

    let okAction = UIAlertAction(title: "OK", style: .default) { (action) in
        self.present(self.imagePicker, animated: true, completion: nil)
    }
    let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)

    alert.addAction(okAction)
    alert.addAction(cancelAction)

    present(alert, animated: true, completion: nil)
}

@IBAction private func attachImageGesture(_ sender: UILongPressGestureRecognizer) {
    let alert = UIAlertController(title: "画像を添付", message: nil, preferredStyle: .actionSheet)

    let okAction = UIAlertAction(title: "OK", style: .default) { (action) in
        self.present(self.imagePicker, animated: true, completion: nil)
    }
    alert.addAction(okAction)

    let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
    alert.addAction(cancelAction)

    present(alert, animated: true, completion: nil)
}

まとめ

ここまでリーダブルコードを参考にコードを改善してきましたが、本書は王道で基本的なリファクタリングを学べる本だと分かりました。

劇的にコードの量を減らして、誰もが理解できる美しいコードを書ける方法など書いてありません。

単純に
・変数、関数の名前を変える
・分かりやすいコメントを加える
・処理の順番を変える
など、簡単な方法ばかりでした。

しかし、リファクタリング=コードを綺麗にする、とは、地道な作業ばかりです。

簡単な方法だからこそ、何時も忘れてはならない、当たり前の方法だと思います。

本書の内容は技術書の中では読みやすいですが、基本に忠実なリファクタリングの方法が載っています。

今後エンジニアとして生きていくなら、本書からリファクタリングを学ぶのがおすすめです。

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

[swift5]tabBarをタップした場合に処理を実行する方法

didSelectを使用しよう

https://developer.apple.com/documentation/uikit/uitabbardelegate/1623463-tabbar
Appleの公式リファレンスです。

tabBarをタップした場合に処理を実行したい場合は、dedSelectを使いましょう。

ViewController.swift
//tabbarをタップした場合のアクション
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
  switch item.title {
    case "ニュース":
      print("ニュースアイテムをタップしました")
    case "天気":
      print("天気アイテムをタップしました")
    case "検索":
      print("検索アイテムをタップしました")
    default: break
    }
}

今回のサンプルコードではswitch文でtabBarのitemのtitleごとに処理を分けています。
item.titleがそれを意味します。

tabBarは左からtag [0.1.2...]と数字で管理されていますが、私は、第三者がコードを見た場合に、tagで分岐しているよりも、item.titleで分岐している方がわかりやすい、と考えているためitem.titleとしています。

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

特定の角のみ ContainerRelativeShape を適用したい [SwiftUI]

iOS 14 から登場した自動角丸 ContainerRelativeShape を、特定の角にだけ利用したいという記事です。

ContainerRelativeShape とは

iPhone 11 の端末自体や、 iOS14 から登場した Widget など、各箇所固有の角丸UIが増えてきています。
それぞれの箇所で適切な角丸半径で表示してくれるのが、ContainerRelativeShape です。

目的

左上に配置して ContainerRelativeShape 適用 拡張して左上だけ角丸にする!
  • 特定の角だけ角丸にしたい
  • なおかつただの角丸ではなく自動計算の ContainerRelativeShape を使いたい

これを実現した、一番右の画像の実装を解説します。

コード

カスタム Shape

Shape Protocol を用い、
角の情報を扱うちょうどよい UIRectCorner OptionSet があるので利用して、
独自の ContainerRelativeShapeSpecificCorner Struct を定義します。

struct ContainerRelativeShapeSpecificCorner: Shape {

    private let corners: [UIRectCorner]

    init(corner: UIRectCorner...) {
        self.corners = corner
    }

    func path(in rect: CGRect) -> Path {
        var p = ContainerRelativeShape().path(in: rect)

        if corners.contains(.allCorners) {
            return p
        }

        if !corners.contains(.topLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.topRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        return p
    }
}

利用例

// 本来
Image("camera")
    .clipShape(ContainerRelativeShape())

// 今回のやつ (特定の角だけ角丸)
Image("camera")
    .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft, .topRight))

// 全コードサンプル
struct SampleView: View {
    var body: some View {
        Group {
            Image("camera")
                .resizable()
                .scaledToFill()
                .frame(width: 80, height: 80)
                .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .padding(8)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

特定の角のみ ContainerRelativeShape を適用したい [SwiftUI]

iOS 14 から登場した自動角丸 ContainerRelativeShape を、特定の角にだけ利用したいという記事です。

ContainerRelativeShape とは

iPhone 11 の端末自体や、 iOS14 から登場した Widget など、各箇所固有の角丸UIが増えてきています。
それぞれの箇所で適切な角丸半径で表示してくれるのが、ContainerRelativeShape です。

目的

左上に配置して ContainerRelativeShape 適用 拡張して左上だけ角丸にする!
  • 特定の角だけ角丸にしたい
  • なおかつただの角丸ではなく自動計算の ContainerRelativeShape を使いたい

これを実現した、一番右の画像の実装を解説します。

コード

カスタム Shape

Shape Protocol を用い、
角の情報を扱うちょうどよい UIRectCorner OptionSet があるので利用して、
独自の ContainerRelativeShapeSpecificCorner Struct を定義します。

struct ContainerRelativeShapeSpecificCorner: Shape {

    private let corners: [UIRectCorner]

    init(corner: UIRectCorner...) {
        self.corners = corner
    }

    func path(in rect: CGRect) -> Path {
        var p = ContainerRelativeShape().path(in: rect)

        if corners.contains(.allCorners) {
            return p
        }

        if !corners.contains(.topLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.topRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomLeft) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        if !corners.contains(.bottomRight) {
            p.addPath(Rectangle().path(in: CGRect(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2, width: rect.width / 2, height: rect.height / 2)))
        }
        return p
    }
}

利用例

// 本来
Image("camera")
    .clipShape(ContainerRelativeShape())

// 今回のやつ (特定の角だけ角丸)
Image("camera")
    .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft, .topRight))

// 全コードサンプル
struct SampleView: View {
    var body: some View {
        Group {
            Image("camera")
                .resizable()
                .scaledToFill()
                .frame(width: 80, height: 80)
                .clipShape(ContainerRelativeShapeSpecificCorner(corner: .topLeft))
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .padding(8)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む