20200220のiOSに関する記事は6件です。

Neumorphismの電卓を作ってみる

Neumorphismの電卓

Neumorphismのデザインが素敵だったので,練習で仲間と一緒に電卓を作ってみました!
とりあえず動くようになったので公開しようと思います。
ほとんど彼につくってもらいましたが...

image.png

この電卓は,主にNeumorLabelNeumorButtonの2つから作られています。

Neumorphismとは

参考

https://note.com/hironobukimura/n/n0431c73714e8

ボタンなどの要素が凹凸で表現されている,新しいスキューモーフィックデザインのことみたいです。

NeumorLabel

コード全体

swift/NeumorLabel.swift
import Foundation
import UIKit

class NeumorLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)
        label()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        label()
    }

    private let params = Params()

    private let topAndBottomLayer = CAGradientLayer(),
                leftAndRightLayer = CAGradientLayer(),
                calcResultLabel = UILabel()

    private func label() {
        putTopAndBottom()
        putLeftAndRight()
        addSubview(calcResultLabel)
    }
    private func putTopAndBottom() {
        topAndBottomLayer.cornerRadius = params.LABEL_CORNER_RADIUS
        topAndBottomLayer.frame = self.bounds
        topAndBottomLayer.colors = [
            params.BACKGROUND_COLOR.darker().cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.BACKGROUND_COLOR.brighter().cgColor
        ]
        topAndBottomLayer.locations = [
            0,
            0.15,
            0.85,
            1
        ]
        topAndBottomLayer.opacity = 1
        layer.insertSublayer(topAndBottomLayer, at: 0)
    }
    private func putLeftAndRight() {
        leftAndRightLayer.cornerRadius = params.LABEL_CORNER_RADIUS
        leftAndRightLayer.frame = self.bounds
        leftAndRightLayer.colors = [
            params.BACKGROUND_COLOR.darker().cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.BACKGROUND_COLOR.brighter().cgColor
        ]
        leftAndRightLayer.locations = [
            0,
            0.05,
            0.95,
            1
        ]
        leftAndRightLayer.startPoint = CGPoint(x: 0, y: 0)
        leftAndRightLayer.endPoint = CGPoint(x: 1, y: 0)
        leftAndRightLayer.opacity = 0.5
        layer.insertSublayer(leftAndRightLayer, at: 1)
    }
    func putUILabel(text: String) {
        calcResultLabel.text = text
        calcResultLabel.frame = CGRect(
            x: 15,
            y: 0,
            width: self.bounds.width * 0.9,
            height: self.bounds.height
        )
        calcResultLabel.textAlignment = .right
        calcResultLabel.font = UIFont(name: "Roboto-Bold", size: 30)
    }
}

コードのざっくりとした解説

storyboardで指定するためのクラスNeumorLabel.swiftを作成しました。
CAGradientLayerをただ上につけるだけだと文字が隠れちゃって表示されない現象が起きてしまったので,putUILabel()でさらに上にUILabelを載せられるようにしています。(ViewControllerからputUILabel()は参照しています)
Layerは影と光を表現するために使っています。左ななめ上から光があたってる感じになっているます。それぞれ白色と黒色で再現しています。

NeumorSquareButton

練習で円いボタンも作成していたので,名前にわざと"Square"がついています。

コード全体

swift/NeumorSquareButton.swift
import Foundation
import UIKit

let FONT_SIZE = 16

class NeumorSquareButton: UIButton {
    override init(frame: CGRect){
        super.init(frame: frame)
        button()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        button()
    }
    private let params = Params()

    let highlightLayer = CALayer(),
        shadowLayer = CALayer(),
        textLayer = CATextLayer()

    func button() {
        setTitleColor(UIColor.clear, for: .normal)
        textLayer.string = currentTitle
        textLayer.foregroundColor = UIColor.gray.cgColor
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.fontSize = CGFloat(FONT_SIZE)
        textLayer.alignmentMode = .center

        [highlightLayer, shadowLayer, textLayer].forEach {
            $0.masksToBounds = false
            $0.frame = layer.bounds
        }

        textLayer.frame = CGRect(
            x: CGFloat(0),
            y: self.bounds.height/2 - CGFloat(FONT_SIZE/2),
            width: self.bounds.width,
            height: CGFloat(FONT_SIZE)
        )

        putHighlight()
        putShadow()

        layer.addSublayer(shadowLayer)
        layer.addSublayer(highlightLayer)
        layer.addSublayer(textLayer)
    }

    private func putHighlight() {
        highlightLayer.backgroundColor = params.NORMAL_BUTTON_BGCOLOR.cgColor
        highlightLayer.shadowColor = params.BACKGROUND_COLOR.brighter().cgColor
        highlightLayer.cornerRadius = layer.frame.size.height * 0.15
        highlightLayer.shadowOpacity = 1
        highlightLayer.shadowOffset = CGSize(width: -6, height: -6)
        highlightLayer.shadowRadius = 10
    }
    private func putShadow() {
        shadowLayer.backgroundColor = UIColor.white.cgColor
        shadowLayer.shadowColor = UIColor.black.cgColor
        shadowLayer.cornerRadius = layer.frame.size.height * 0.15
        shadowLayer.shadowOpacity = 0.15
        shadowLayer.shadowOffset = CGSize(width: 6, height: 6)
        shadowLayer.shadowRadius = 10
    }

    func buttonPush() {
        highlightLayer.shadowOpacity = 0
        shadowLayer.shadowOpacity = 0

        let frame = CGRect(x: 0, y: 0, width: layer.frame.size.width, height: layer.frame.size.height)
        let gradColors = [
            params.BACKGROUND_COLOR.darker().cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.NORMAL_BUTTON_BGCOLOR.cgColor,
            params.BACKGROUND_COLOR.brighter().cgColor
        ]
        let gradLocations = [0, 0.15, 0.85, 1]
        let shadowOpacity: Float = 0.5
        let cornerRadius = layer.frame.size.height * 0.15

        let virticalGradLayer = CAGradientLayer()
            virticalGradLayer.opacity = shadowOpacity
            virticalGradLayer.frame = frame
            virticalGradLayer.colors = gradColors
            virticalGradLayer.locations = gradLocations as [NSNumber]
            virticalGradLayer.cornerRadius = cornerRadius
        let horizonGradLayer = CAGradientLayer()
            horizonGradLayer.opacity = shadowOpacity
            horizonGradLayer.frame = frame
            horizonGradLayer.startPoint = CGPoint(x: 0, y: 0)
            horizonGradLayer.endPoint = CGPoint(x: 1, y: 0)
            horizonGradLayer.colors = gradColors
            horizonGradLayer.locations = gradLocations as [NSNumber]
            horizonGradLayer.cornerRadius = cornerRadius

        layer.addSublayer(horizonGradLayer)
        layer.addSublayer(virticalGradLayer)
        layer.addSublayer(textLayer)
    }
}

コードのざっくりとした解説

押されたときのアニメーションを地道につけていってます...
押されたときのアニメーション↓

https://www.youtube.com/watch?v=gtr46NjJOls

Labelのときと同じように影と光を配置しています。

おわりに

ただ電卓を公開したかっただけだったので,一応githubのリポジトリを貼っておきます。
https://github.com/Papillon6814/neumor-calculator/blob/master/README.md

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

Face IDを使ってみた開発メモ(Objective-C編)

事前準備

1.プロジェクトから「info」→「Custom iOS Target Properties」に
 「Privacy - Face ID Usage Description 」を追加する。
 内容はFaceIDの使用を許可するダイアログメッセージに使用される。
スクリーンショット 2020-02-20 16.32.42.png

2.プロジェクトにLocalAuthetication.frameworkを追加する。
「Build Phases」→「Link Binary With Libraries」→「+」ボタンから追加
スクリーンショット 2020-02-19 17.57.23.png

3.使用するクラスにimportを追加

import "LocalAuthentication/LocalAuthentication.h"

使用するメソッドの説明

- (BOOL)canEvaluatePolicy:(LAPolicy)policy 
                    error:(NSError * _Nullable *)error;

動かしている端末が指定の生体認証に対応しているかチェックするメソッド。
指定する「LAPolicy」には以下のようなパターンがある。

LAPolicy 内容
LAPolicyDeviceOwnerAuthenticationWithBiometrics 生体認証
LAPolicyDeviceOwnerAuthenticationWithWatch AppleWatchによる認証
LAPolicyDeviceOwnerAuthenticationWithBiometricsOrWatch 生体認証かAppleWatchのいずれかによる認証
LAPolicyDeviceOwnerAuthentication 端末で認証として設定しているいずれかの認証

今回は生体認証が使用できるかどうかをチェックするので
「LAPolicyDeviceOwnerAuthenticationWithBiometrics」を使用する。
「error」は「LAError」で返ってくる。詳細は参考から。

- (void)evaluatePolicy:(LAPolicy)policy 
       localizedReason:(NSString *)localizedReason 
                 reply:(void (^)(BOOL success, NSError *error))reply;

指定した生体認証を行うメソッド。
「localizedReason」には実際に生体認証を行う際のダイアログメッセージとして利用される。
生体認証の成否は「success」でtrue/falseで返ってくる。
こちらも「error」には「LAError」が返ってくる。詳細は参考から。

サンプル

    LAContext *context = [[LAContext alloc] init];
    NSError *authError = nil;

    if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError])
    {
        [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
                  localizedReason:@"今回はテストでFaceIDを使用します"
                            reply:^(BOOL success, NSError *error)
         {
             if (success) {
                 NSLog(@"ログインに成功しました");
             } else {
                 NSLog(@"ログイン出来ませんでした。");
             }
         }];
    } else {
        NSLog(@"FaceIDに対応していない端末でした");
    }

参考

公式ドキュメント
https://developer.apple.com/documentation/localauthentication/lacontext?language=objc

LAErrorについて
https://developer.apple.com/documentation/localauthentication/lapolicy/lapolicydeviceownerauthentication?language=objc

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

iOSとAndroidのダークモード検証を楽にしよう

ダークモードの検証忘れて文字が見えない!

はい、これ良くやらかしてませんか?
最近 iOSもAndroidもダークモードが追加され、デフォルトからを背景画像や文字色に使用すると、OSのダークモード設定に合わせて自動でカラーの反転がされますよね。それで、背景は白固定にしてるのに、文字色だけデフォルトにしていて文字が見えない!ということが起きます。

開発時・テスト時のダークモード検証を楽にしよう

ダークモードのテストするとき、毎度設定画面を開くのって面倒ですよね。少ないアクションで相互に切り替えれると便利です。実はiOSのAndroidもコントロールセンター(画面上からしたスワイプして出てくる画面)に下記のようにダークモードの切り替えボタンを追加できます!これすごく便利だと思いませんか?

iOS

IMG_0FD6639A30A5-1.png

Android

IMG_0FD6639A30A5-1.png

設定方法

以下、各OSでの設定方法をまとめておきます。OSのバージョンや、Androidだと端末が異なると設定方法も変わるので、環境が違う場合は自身の環境に合わせて調べて見てくださいね。

iOS (iPhone XR / 13.3.1)

設定アプリTop コントロールセンター カスタマイズ
IMG_0244.png IMG_0245.png IMG_0246.png

Android (Pixel 4 XL / Andriod 10)

クイック設定パネル カスタマイズ
Screenshot_20200220-115239.png Screenshot_20200220-115329.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NSURLErrorDomainのコード一覧

NSURLErrorDomain のコード一覧を置いておきますね。ログでコードでてきてもすぐに結びつかないので...。

name value
NSURLErrorUnknown -1
NSURLErrorCancelled -999
NSURLErrorBadURL -1000
NSURLErrorTimedOut -1001
NSURLErrorUnsupportedURL -1002
NSURLErrorCannotFindHost -1003
NSURLErrorCannotConnectToHost -1004
NSURLErrorNetworkConnectionLost -1005
NSURLErrorDNSLookupFailed -1006
NSURLErrorHTTPTooManyRedirects -1007
NSURLErrorResourceUnavailable -1008
NSURLErrorNotConnectedToInternet -1009
NSURLErrorRedirectToNonExistentLocation -1010
NSURLErrorBadServerResponse -1011
NSURLErrorUserCancelledAuthentication -1012
NSURLErrorUserAuthenticationRequired -1013
NSURLErrorZeroByteResource -1014
NSURLErrorCannotDecodeRawData -1015
NSURLErrorCannotDecodeContentData -1016
NSURLErrorCannotParseResponse -1017
NSURLErrorAppTransportSecurityRequiresSecureConnection -1022
NSURLErrorFileDoesNotExist -1100
NSURLErrorFileIsDirectory -1101
NSURLErrorNoPermissionsToReadFile -1102
NSURLErrorDataLengthExceedsMaximum -1103
NSURLErrorFileOutsideSafeArea -1104
NSURLErrorSecureConnectionFailed -1200
NSURLErrorServerCertificateHasBadDate -1201
NSURLErrorServerCertificateUntrusted -1202
NSURLErrorServerCertificateHasUnknownRoot -1203
NSURLErrorServerCertificateNotYetValid -1204
NSURLErrorClientCertificateRejected -1205
NSURLErrorClientCertificateRequired -1206
NSURLErrorCannotLoadFromNetwork -2000
NSURLErrorCannotCreateFile -3000
NSURLErrorCannotOpenFile -3001
NSURLErrorCannotCloseFile -3002
NSURLErrorCannotWriteToFile -3003
NSURLErrorCannotRemoveFile -3004
NSURLErrorCannotMoveFile -3005
NSURLErrorDownloadDecodingFailedMidStream -3006
NSURLErrorDownloadDecodingFailedToComplete -3007
NSURLErrorInternationalRoamingOff -1018
NSURLErrorCallIsActive -1019
NSURLErrorDataNotAllowed -1020
NSURLErrorRequestBodyStreamExhausted -1021
NSURLErrorBackgroundSessionRequiresSharedContainer -995
NSURLErrorBackgroundSessionInUseByAnotherProcess -996
NSURLErrorBackgroundSessionWasDisconnected -997
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】UIButtonにボーダーラインを引けるようにする

4辺に線を引けるようにしたUIButtonを紹介します。
image.png

色と太さのほか、各辺の開始点・終了点のオフセットを指定できます。
image.png

画面回転もOK。(わかりづらい)
image.png

縦に並べる時にけっこう良い。
image.png

コード

import UIKit

class BorderedButton: UIButton {
    private var topBorder: CALayer = CALayer()
    private var bottomBorder: CALayer = CALayer()
    private var leftBorder: CALayer = CALayer()
    private var rightBorder: CALayer = CALayer()
    private var layers: [CALayer] = []
    private var widths: [CGFloat] = [0, 0, 0, 0]
    private var startOffsets: [CGFloat] = [0, 0, 0, 0]
    private var endOffsets: [CGFloat] = [0, 0, 0, 0]


    override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setUp()
    }
    private func setUp() {
        layers = [topBorder, bottomBorder, leftBorder, rightBorder]
        layers.forEach {
            $0.backgroundColor = UIColor(red: 221/255, green: 221/255, blue: 221/255, alpha: 0.92).cgColor
            self.layer.addSublayer($0)
        }
    }
    func setBorderColor(_ color: UIColor) {
        layers.forEach {
            $0.backgroundColor = color.cgColor
        }
    }
    func setTopBorder(width: CGFloat, startOffset: CGFloat, endOffset: CGFloat) {
        widths[0] = width
        startOffsets[0] = startOffset
        endOffsets[0] = endOffset
    }
    func setBottomBorder(width: CGFloat, startOffset: CGFloat, endOffset: CGFloat) {
        widths[1] = width
        startOffsets[1] = startOffset
        endOffsets[1] = endOffset
    }
    func setLeftBorder(width: CGFloat, startOffset: CGFloat, endOffset: CGFloat) {
        widths[2] = width
        startOffsets[2] = startOffset
        endOffsets[2] = endOffset
    }
    func setRightBorder(width: CGFloat, startOffset: CGFloat, endOffset: CGFloat) {
        widths[3] = width
        startOffsets[3] = startOffset
        endOffsets[3] = endOffset
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        layoutBorderFrame()
    }
    private func layoutBorderFrame() {
        topBorder.frame = CGRect(x: startOffsets[0], y: 0, width: self.frame.width - (startOffsets[0] + endOffsets[0]), height: widths[0])
        bottomBorder.frame = CGRect(x: startOffsets[1], y: self.frame.height - widths[1], width: self.frame.width - (startOffsets[1] + endOffsets[1]), height: widths[1])
        leftBorder.frame = CGRect(x: 0, y: startOffsets[2], width: widths[2], height: self.frame.height - (startOffsets[2] + endOffsets[2]))
        rightBorder.frame = CGRect(x: self.frame.width - widths[3], y: startOffsets[3], width: widths[3], height: self.frame.height - (startOffsets[3] + endOffsets[3]))
    }
}

使い方

以下のように太さ・オフセット・色を指定できます。
image.png

let button = BorderedButton()
button.setTopBorder(width: 4, startOffset: 240, endOffset: 0)
button.setRightBorder(width: 36, startOffset: 0, endOffset: 0)
button.setLeftBorder(width: 36, startOffset: 0, endOffset: 0)
button.setBottomBorder(width: 4, startOffset: 0, endOffset: 240)
button.setBorderColor(UIColor.orange)
button.setTitle("Hello Bordered Button", for: .normal)
button.setTitleColor(.black, for: .normal)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ARKit】childNode(withName:recursively:)でThread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

はじめに

こちらを参考にARKitで3Dモデルを表示しようとしたところ、「 Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value」のエラーが出たので、その対処を備忘録として。

実行環境

  • Xcode 11.2.1
  • MagicaVoxel 0.99.4
  • Mixamo

コード

上記リンクのサンプルはボタンで使うscnを切り替えていたのですが、簡単にテストするために変数を「swing」固定としました。また SceneKit Catalog の名前も変更(art.scnassets)してあります。

ViewController.swift
var selectedItem: String? = "swing"
//(中略)
let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn")

エラーになった箇所

以下のfunctionの childNode(withName:recursively:) で 「 Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value」のエラーが出ました。

addItem
func addItem(hitTestResult: ARHitTestResult) {
        if let selectedItem = self.selectedItem {

            // アセットのより、シーンを作成
            let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn")

            // ノード作成
            let node = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))!

            // 現実世界の座標を取得
            let transform = hitTestResult.worldTransform
            let thirdColumn = transform.columns.3

            // アイテムの配置
            node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z)
            self.mainSceneView.scene.rootNode.addChildNode(node)
        }
    }

原因

SCNScene.childNode(withName:recursively:) は、SCNScene から withNname で指定した名前のノードを取得するメソッドですが、swing.scnに "swing" というノードが存在しないためでした。
cap00.png

対処

swing.scnを開いて、Scene graphパネルの左下の「+」ボタンでノードを追加して「swing」に名前を変更して、元々あったノードを「swing」ノードの配下にドラッグして移動します。
cap01.png

3Dオブジェクトのサイズ調整

3Dオブジェクトのサイズが大きすぎたので、サイズ変更のコードを追加してあります。
node.scale = SCNVector3(0.05, 0.05, 0.05)

addItem
    /// アイテム配置メソッド
    func addItem(hitTestResult: ARHitTestResult) {
        if let selectedItem = self.selectedItem {

            // アセットのより、シーンを作成
            let scene = SCNScene(named: "art.scnassets/\(selectedItem).scn")

            // ノード作成
            let node = (scene?.rootNode.childNode(withName: selectedItem, recursively: false))!

            // 現実世界の座標を取得
            let transform = hitTestResult.worldTransform
            let thirdColumn = transform.columns.3

            // アイテムの配置
            node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z)

            // アイテムのサイズを変更
            node.scale = SCNVector3(0.05, 0.05, 0.05)

            self.mainSceneView.scene.rootNode.addChildNode(node)
        }
    }

ちなみに MagicaVoxelで作ったときのサイズは、21 21 21 でした。
cap02.png

この辺の適正サイズや,sncファイルのscale値で変更した方がいいのかは、現時点ではちょっとわからず。。。

まとめ

前提の知識(3Dモデルとか)が乏しいのでいろいろ躓いています。他の3Dアプリで作ったモデルはちゃんとノード名がつくんですかね。。。

MagicaVoexlについてはこちらを参考にしました。やはり公開することを考えると、オリジナルのモデルが必要になるので、簡単でいいです。

MagicaVoexlにアニメーションをつけるのには、Maximoを使いました。こちらもモデルをアップロードしてアニメーション選ぶだけなので、これまた簡単です。こちらを参考にしました(モデルをMagicaVoexlで作る場合は、Blender部分はすっ飛ばして大丈夫です)。

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