しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

PHPickerViewControllerで画像を取得する

PHPickerViewControllerというiOS14から登場したクラスで画像を取得してみました。
このクラスはUIImagePickerControllerと違って複数画像を選択することなども可能です。

まずは画面に画像選択用のボタンを設置します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("画像選択", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action:#selector(self.tapButton), for: .touchUpInside)
        button.sizeToFit()
        button.center = view.center
        view.addSubview(button)
    }
    
    @objc func tapButton() {
    }
}

画面の真ん中にボタンを設置できました。

f:id:llcc:20210514115016p:plain

次はボタンを押すとPHPickerViewControllerが立ち上がるようにします。
画像選択可能数などの設定はPHPickerConfigurationを使って行っています。

import PhotosUI

class ViewController: UIViewController {
    override func viewDidLoad() {
        // 省略
    }
    
    @objc func tapButton() {
        var config = PHPickerConfiguration()
        config.filter = .images
        config.selectionLimit = 0
        let pickerView = PHPickerViewController(configuration: config)
        present(pickerView, animated: true, completion: nil)
    }
}

無事にPickerを表示できました。

f:id:llcc:20210514115227p:plain

最後にUIImageの取得処理を実装します。
画像の取得はPHPickerViewControllerDelegateで行います。

class ViewController: UIViewController {
    @objc func tapButton() {
        let pickerView = PHPickerViewController(configuration: config)
        pickerView.delegate = self // 追加
        present(pickerView, animated: true, completion: nil)
    }
}

extension ViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        results.forEach {
            $0.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
                print(image)
            }
        }
    }
}

ログを見ると正しく画像が選択できている事が分かります。

f:id:llcc:20210514115407p:plain

注意点ですがloadObjectは非同期の為、読み込み完了順に呼ばれます。
そのため画像選択の順番は保持されません。
もし順番を保持したい場合、保持するような仕組みを実装する必要があります。

AndroidのEditTextやTextViewのフォントを明朝体にする

android:fontFamilyをserifにするだけです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_first_fragment"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:fontFamily="serif" />

f:id:llcc:20210502171809p:plain

日本語も対応しています。

f:id:llcc:20210502171836p:plain

android:typefaceをserifにしても同じ結果になります。
typefaceはboldやitalicを指定できるプロパティーです。

<TextView
    android:id="@+id/textview_first"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="こんにちは"
    app:layout_constraintBottom_toTopOf="@id/button_first"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:typeface="serif" />

【Kotlin】Applicationを継承したクラスを試してみる

AndroidのApplicationクラスを使ってみました。
Applicationを継承したクラスは全Activityからアクセスできるクラスになります。

クラスは下のように定義します。

class MainApplication: Application() {
}

下のようにAndroidManifest.xmlのapplicationタグに追加することでActivityから呼び出せるようになります。

<application android:name=".MainApplication">
</application>

Activityからは下のように呼び出す事ができます。
もしAndroidManifest.xmlへの記載がない場合、applicationにはandroid.app.Applicationクラスのインスタンスが入ります。

val app = (application as MainApplication)

ライフサイクルメソッドとしては、インスタンス作成時に呼ばれるonCreateなどがあります。

class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
    }
}

【Kotlin】NavControllerを試してみる

Empty Activityを選んでプロジェクト作成をします。

f:id:llcc:20210323205236p:plain

起動すると下のようにラベルだけの画面が表示されます。

f:id:llcc:20210323205601p:plain

次はナビゲーションを作成します。 まず新規作成でAndroid Resource Fileを選択します。

f:id:llcc:20210323205925p:plain

Resource TypeをNavigationにしてOKを押します。

f:id:llcc:20210323205857p:plain

my_navigation.xmlが作られたので、次はここに登録するFragmentを2つ作ります。
今回はFirstFragmentとSecondFragmentという2つのフラグメントを作成しました。

f:id:llcc:20210323210344p:plain

Fragmentができたのでmy_navigation.xmlに登録します。
my_navigation.xmlを開いて画面上部の+ボタンからフラグメントを2つ追加します。

f:id:llcc:20210323210437p:plain

下のようにフラグメントが2つ登録されました。

f:id:llcc:20210323210614p:plain

次はナビゲーションを表示します。
activity_main.xmlを下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/my_navigation"
        app:defaultNavHost="true" />

</androidx.constraintlayout.widget.ConstraintLayout>

アプリを起動するとFirstFragmentの内容が表示されました。

f:id:llcc:20210324111557p:plain

次はSecondFragmentへの遷移を実装します。
my_navigation.xmlの右側のActionsの+からActionを作成します。

f:id:llcc:20210324111815p:plain

次はFirstFragmentに画面遷移用のボタンを作成します。
fragment_first.xmlを下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fragment_first_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />
</FrameLayout>

FirstFragment.ktのonViewCreatedにボタン押下時の画面遷移処理を追加します。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    view.findViewById<FloatingActionButton>(R.id.fragment_first_button)?.setOnClickListener {
        findNavController().navigate(R.id.action_firstFragment_to_secondFragment)
    }
}

起動するとボタンだけの画面が表示されます。
このボタンをタップすれば次の画面に移動します。

f:id:llcc:20210324112600p:plain

先程作ったActionを選択してAnimationsの所を編集すれば遷移時のアニメーションを指定できます。

f:id:llcc:20210324122213p:plain

ActionBarの戻るボタンですが、ActivityのsetupActionBarWithNavControllerを呼び出す事で表示できます。
押されたときの動作はonSupportNavigateUpで実装します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupActionBarWithNavController(findNavController(R.id.nav_host_fragment))
    }

    override fun onSupportNavigateUp(): Boolean {
        return findNavController(R.id.nav_host_fragment).navigateUp()
    }
}

f:id:llcc:20210324123037p:plain

次は遷移のときに変数を渡してみます。
まずはそれぞれのbuild.gradleで下のようにプラグインを設定します。

classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4"
apply plugin: "androidx.navigation.safeargs.kotlin"

プラグインを入れたらmy_navigation.xmlを開いて遷移先を選択、画面右側Argumentsの+から変数を追加します。

f:id:llcc:20210324123624p:plain

この状態でビルドするとFirstFragmentDirectionsというクラスが生成されます。
画面遷移時のnavigateメソッドの引数にこのクラスを渡すようにします。

findNavController().navigate(FirstFragmentDirections.actionFirstFragmentToSecondFragment("testValue"))

遷移先では自動生成されたSecondFragmentArgsというクラスを使って変数を取り出します。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    arguments?.let {
        val args = SecondFragmentArgs.fromBundle(it)
        Log.d("test", "${args.testValue}")
    }
}

ログを見ると無事に渡せていることが分かります。

f:id:llcc:20210324125220p:plain

もし上記クラスが自動生成されない場合、Android studioの「Sync Project with Gradle Files」を選択すると良いかもしれません。

f:id:llcc:20210324124848p:plain

SF Symbols + UIImageでアイコンを表示する

SF SymbolsというAppleの提供しているアイコンを使える機能を試してみました。

developer.apple.com

SF Symbols + UIImageでアイコンを表示する

使い方は簡単で、下のようにUIImageの初期化でアイコン名を渡すだけです。
引数のラベルがUIImage(name: "")ではなくUIImage(systemName: "")なのでそこだけ注意して下さい。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let imageView = UIImageView(image: UIImage(systemName: "doc.badge.gearshape.fill"))
        imageView.frame.size = CGSize(width: 100, height: 100)
        imageView.center = view.center
        view.addSubview(imageView)
    }
}

実行すると下のようにアイコンを表示できます。

f:id:llcc:20210107145814p:plain

アイコン名を確認したい時は下URLからアプリをダウンロードして下さい。

developer.apple.com

アプリを起動すると下のようにアイコン一覧とアイコン名が表示されます。

f:id:llcc:20210107150047p:plain

画像の色を変えたい場合、下のようにtintColorを変更します。

imageView.tintColor = .red

画像が赤になりました。

f:id:llcc:20210107150609p:plain

画像はベクターデータの為、大きくしても劣化しません。
下はサイズを500x500に変更したものです。

f:id:llcc:20210107150540p:plain

SF Symbols + UILabelで文字の中にアイコンを入れる

SF Symbolsは画像でしか取れないので、文字の中に入れたい場合は下のようにNSTextAttachmentを使います。

let label = UILabel()
let text = NSMutableAttributedString(string: "アイコン前")
text.append(NSAttributedString(attachment: NSTextAttachment(image: UIImage(systemName: "doc.badge.gearshape.fill")!)))
text.append(NSAttributedString(string: " 後"))
label.attributedText = text
label.sizeToFit()
label.center = view.center
view.addSubview(label)

実行すると文字の間にアイコンが入った事が分かります。

f:id:llcc:20210107151055p:plain

参考URL

Apple Developer Documentation

iOS13のカスタムフォントを試してみる

iOS13から使えるようになっていたカスタムフォントのAPIを試してみました。
この機能を使うと端末へのフォントインストールやフォントの確認ができます。

まずはフォント一覧の取得をしてみようと思います。 最初にテスト用のプロジェクトを作ります。

f:id:llcc:20201031141030p:plain

プロジェクトを作ったらプロジェクト設定の「Signing & Capabilities」でFontsを有効化します。

f:id:llcc:20201031141015p:plain

Storyboardで下のようにラベルとボタンだけ配置します。

f:id:llcc:20201031142117p:plain

最後にボタンを押したらUIFontPickerViewController(フォント一覧画面)を表示するような実装をします。

class ViewController: UIViewController {
    @IBAction func tapBtn() {
        let v = UIFontPickerViewController()
        present(v, animated: true, completion: nil)
    }
}

アプリを起動してボタンを押すとフォント一覧を見る事ができました。

f:id:llcc:20201031143020p:plain

次に一覧からフォントを選んで使います。
コードは下のとおりです。
ViewControllerをUIFontPickerViewControllerDelegateに準拠させました。

class ViewController: UIViewController {
    @IBOutlet weak var testLabel: UILabel!
    
    @IBAction func tapBtn() {
        let v = UIFontPickerViewController()
        v.delegate = self
        present(v, animated: true, completion: nil)
    }
}

extension ViewController: UIFontPickerViewControllerDelegate {
    func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
        guard let fontDescriptor = viewController.selectedFontDescriptor else {
            return
        }
        testLabel.font = UIFont(descriptor: fontDescriptor, size: 17)
    }
}

これでフォント選択したらラベルのフォントが変わるようになりました。

f:id:llcc:20201031143619p:plain

フォント一覧ですが、filteredLanguagesPredicateを使うと日本語対応フォントのみといった絞り込みができます。

let v = UIFontPickerViewController()
v.configuration.filteredLanguagesPredicate = UIFontPickerViewController.Configuration.filterPredicate(forFilteredLanguages: ["js"])

次はフォントのインストールをしてみます。
フォントはNikkyou Sansというフォントを使わせて頂きました。

Nikkyou Sans Font | daredemotypo | FontSpace

ダウンロードしたフォントをxcassetsに追加します。

f:id:llcc:20201031151439p:plain

フォントのインストール処理は下のとおりです。
CTFontManagerRegisterFontsWithAssetNamesメソッドでxcassetsのフォントを登録できます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        CTFontManagerRegisterFontsWithAssetNames(["NikkyouSans-B6aV"] as CFArray, nil, .user, true, { arr, result in
            print(arr)
            print(result)
            return true
        })
    }
}

アプリを起動すると下のようにインストール確認画面が表示されます。

f:id:llcc:20201031151602p:plain

インストールしたフォントは下のようにフォント一覧画面で選べるようになります。

f:id:llcc:20201031151713p:plain

【SwiftUI】GeometryReaderでViewのサイズを取得する

画面サイズによって処理を変えたかったため、GeometryReaderというクラスを使って画面サイズを取得しました。

Viewのサイズは下の形で取得できます。

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            Text(String("\(geometry.size)"))
        }
    }
}

実行すると下のようにViewのサイズが取得できます。

f:id:llcc:20201028105427p:plain

ただ上記ViewサイズはSafeAreaが除外されているので注意が必要です。
SafeAreaを取得したい場合はしたのように記述します。

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            Text(String("\(geometry.safeAreaInsets)"))
        }
    }
}

View全体ではなく、内側にあるViewのサイズを取りたい場合は下のように入れ子にします。

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer().frame(height: 100)
            GeometryReader { geometry in
                Text(String("\(geometry.size)"))
            }
            Spacer().frame(height: 100)
        }
    }
}

実行すると内側のViewのサイズを取れました。

f:id:llcc:20201028110442p:plain

GeometryReaderはWidget上でも使う事ができます。

f:id:llcc:20201028110852p:plain

frameメソッドを使う事で、xとyも取得できます。
引数はCoordinateSpaceというクラスです。

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            Text(String("\(geometry.frame(in: .global))"))
        }
    }
}

実行結果は下のとおりです。
SafeAreaも含めた結果になるので注意が必要です。

f:id:llcc:20201028111312p:plain

CoordinateSpaceは自分でcoordinateSpaceメソッドを使って定義する事も可能です。
下の例ではVStackでTestという名前のCoordinateSpaceを定義して、それに対する位置を取得しています。

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer().frame(height: 100)
            GeometryReader { geometry in
                Text(String("\(geometry.frame(in: .named("Test")))"))
            }
            Spacer().frame(height: 100)
        }.coordinateSpace(name: "Test")
    }
}

実行するとVStackに対する位置情報が取得できている事が分かります。

f:id:llcc:20201028111637p:plain