20200818のSwiftに関する記事は7件です。

APIKitなどのライブラリを使わずに標準のURLSessionで通信処理を実装をする

✔️実装パターン①

RequestとResponseの関係を一対一にする。

Protocolでリクエストを作成

import Foundation

protocol APIRequest {
    associatedtype Responses: Decodable
    associatedtype Parameters: Encodable

    var path: String { get }
    var method: String { get }
    var headers: String? { get }
    var queries: [URLQueryItem]? { get set }
    var body: Parameters? { get set }
}

struct Request {
    struct Login: APIRequest {
        typealias Responses = UserResponse
        typealias Parameters = UserRequest

        let path = "/login"
        let method = "POST"
        let headers: String? = nil
        var queries: [URLQueryItem]?
        var body: UserRequest?
    }

    struct Signup: APIRequest {
        typealias Responses = UserResponse
        typealias Parameters = UserRequest

        let path = "/sign_up"
        let method = "POST"
        let headers: String? = nil
        var queries: [URLQueryItem]?
        var body: UserRequest?
    }
}

UserRequest

import Foundation

struct UserRequest: Encodable {
    var email: String
    var password: String
}

UserResponse

import Foundation

struct UserResponse: Decodable {
    var result: User

    struct User: Decodable {
        var id: Int
        var email: String
        var token: String
    }
}

APIClient

APIRequestに準拠している構造体を引数に入れる。
typeによって処理を切り替える。

import Foundation

struct APIClient {
    static func sendRequest<T: APIRequest>(
        from type: T,
        completion: @escaping (Result<T.Responses, Error>) -> Void) {

        let BaseURL: String = "http://xxxxxxxxx"

        func createRequest() -> URLRequest? {
            guard var components = URLComponents(string: "\(BaseURL)\(type.path)") else { return  nil}

            if type.method == "GET" {
                let queryItems = type.queries
                components.queryItems = queryItems
            }

            guard let url = components.url else { return nil}
            var request = URLRequest(url: url)
            request.httpMethod = type.method

            if type.method != "GET" {
                let httpBody = JSONEncoder().encode(value: type.body)
                request.httpBody = httpBody
            }

            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.setValue(type.headers, forHTTPHeaderField: "access_token")
            return request
        }

        guard let request = createRequest() else { return }

        let task = URLSession.shared.dataTask(with: request) { (data, res, error) in
            if let error = error {
                completion(.failure(error))
            }

            guard let data = data else {
                print("no data")
                return
            }

            guard let res = res as? HTTPURLResponse, (200...299).contains(res.statusCode) else { return }

            do {
                let jsonDecoder = JSONDecoder()
                jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
                let json = try jsonDecoder.decode(T.Responses.self, from: data)
                DispatchQueue.main.sync {
                    completion(.success(json))
                }
            } catch {
                DispatchQueue.main.sync {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
    }
}

呼び出し元

import UIKit

class ViewController: UIViewController {

    typealias UserRequest = Request.Login

    override func viewDidLoad() {
        super.viewDidLoad()

        let params = UserRequest.Parameters(
            email: "xxx.com",
            password: "xxxxx"
        )

        let queryItems: [URLQueryItem] = {
            var queryItems = [URLQueryItem]()
            queryItems.append(URLQueryItem(name: "email", value: params.email))
            queryItems.append(URLQueryItem(name: "password", value: params.password))
            return queryItems
        }()

        APIClient.sendRequest(from: UserRequest(queries: queryItems)) { (result) in
            switch result {
            case .success(let response):
                print("success", response)
            case .failure:
                print("failure")
            }
        }
    }
}

extension

import Foundation

extension JSONEncoder {
    func encode<T: Encodable>(value: T) -> Data? {
        self.keyEncodingStrategy = .convertToSnakeCase
        let encodeValue = try? self.encode(value)
        return encodeValue
    }
}

✔️実装パターン② アンチパターン

最初は以下のように書いていたのですが、実はアンチパターンのようでした。
理由としては以下の通りです。

  • Requestに対して引数にResponseの型を入れる。
  • リクエストをする際にリクエスト・レスポンスを入力する必要があると、間違えて入力した際にレスポンスが受け取れなくなる、なのでリクエストとレスポンスは一対一にするのがよさそう。

URLRouter

URLSessionの作成、リクエストを作成する。

// URLの向き先を作成する型、エンドポイントを追加したい時やHTTPの設定はこの型を参照する。
enum URLRouter {
    case addBook
    case editBook(id: Int, body: [String: Any])

    private static let baseURLString = "YOUR_BASE_URL_STRING"

    private enum HTTPMethod {
        case get
        case post
        case put
        case delete

        var value: String {
            switch self {
            case .get: return "GET"
            case .post: return "POST"
            case .put: return "PUT"
            case .delete: return "DELETE"
            }
        }
    }

    private var method: HTTPMethod {
        switch self {
        case .addBook: return .get
        case .editBook: return .put
        }
    }

    private var path: String {
        switch self {
        case .addBook:
            return "/books"
        case .editBook(let id):
             return "/books/\(id)"
        }
    }

    func request() throws -> URLRequest {
        let urlString = "\(URLRouter.baseURLString)\(path)"

        guard let url = URL(string: urlString) else {
            throw ErrorType.parseUrlFail
        }

        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 10)
        request.httpMethod = method.value
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        switch self {
        case .addBook:
            return request
        case .editBook(_, let body):
            request.httpBody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
            return request
        }
    }
}

ネットワーク通信をするクラス

class Network {
    static let shared = Network()

    private let config: URLSessionConfiguration
    private let session: URLSession

    private init() {
        config = URLSessionConfiguration.default
        session = URLSession(configuration: config)
    }

    // リクエスト時にURLRouterを注入する。
    func request<T: Decodable>(router: URLRouter, completion: @escaping (Result<T, Error>) -> ()) {
        do {
            let task = try session.dataTask(with: router.request()) { (data, urlResponse, error) in
                DispatchQueue.main.async {
                    if let error = error {
                        completion(.failure(error))
                        return
                    }

                    guard let statusCode = urlResponse?.getStatusCode(), (200...299).contains(statusCode) else {
                        let errorType: ErrorType

                        switch urlResponse?.getStatusCode() {
                        case 404:
                            errorType = .notFound
                        case 422:
                            errorType = .validationError
                        case 500:
                            errorType = .serverError
                        default:
                            errorType = .defaultError
                        }

                        completion(.failure(errorType))
                        return
                    }

                    guard let data = data else {
                        completion(.failure(ErrorType.defaultError))
                        return
                    }

                    do {
                        let result = try JSONDecoder().decode(T.self, from: data)
                        completion(.success(result))
                    } catch let error {
                        completion(.failure(error))
                    }
                }
            }
            task.resume()

        } catch let error {
            completion(.failure(error))
        }
    }
}

extension URLResponse {
    func getStatusCode() -> Int? {
        if let httpResponse = self as? HTTPURLResponse {
            return httpResponse.statusCode
        }
        return nil
    }
}

enum ErrorType: LocalizedError {
    case parseUrlFail
    case notFound
    case validationError
    case serverError
    case defaultError

    var errorDescription: String? {
        switch self {
        case .parseUrlFail:
            return "Cannot initial URL object."
        case .notFound:
            return "Not Found"
        case .validationError:
            return "Validation Errors"
        case .serverError:
            return "Internal Server Error"
        case .defaultError:
            return "Something went wrong."
        }
    }
}

呼び出し元

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let id = 2525
        // クエリ
        let params: [String: Any] = [
            "first_name": "Amitabh2",
            "last_name": "Bachchan2",
            "email": "ab@bachchan.com",
            "phone_number": "+919980123412",
            "favorite": false
        ]

        Network.shared.request(router: .editBook(id: id, body: params)) { (result: Result<BookEditResponse, Error>) in
            switch result {
            case .success(let item):
                print("成功")
            case .failure(let err):
                print("失敗")
            }
        }
    }
}

少しでも参考になりましたら幸いです?

参考

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

Swift Package Manager とは

概要

Swift Package Manager(以降: SwiftPM) は、Swift コードの配布を管理するためのツールです。また、Swift のビルドシステムと統合されていることにより、依存関係のダウンロード・コンパイルなどが最適化されバイナリ、リソースがプロジェクトで使いやすい形になります。 SwiftPM は既存で存在する依存管理ツールの CocoaPods や Carthage とは違い Apple が OSS とし提供しているツールで、Xcode 9以降(Swift 3.0以降)に含まれています。

image.png

ライブラリを導入してみる

SwiftPM はXcode10 以前は Package.swift などに依存先を記述して、コマンドラインからのみビルド・実行しかできませんでした。そのため、サーバサイドや CUI アプリケーションなどでしか利用シーンがありませんでした。しかし、Xcode 11 以降からは .xcodeproj に直接依存先の情報などを記述して導入できるようになったので、iOS アプリケーションでも SwiftPM が利用できるようになりました。つまり、CLI としてライブラリを利用する(SwiftFormatなど) + CLI ライブラリ、サーバサイドアプリなどを作成する以外は基本的には、Package.swift を編集する必要はないということです。

Lottie をインストール

File > Swift Packages > Add Package Dependency.. を開きます。

image.png

Choose Package Repository 画面がでたら lottie-ios のリポジトリ URL を入力して次に進みます。

image.png

次にバイナリを指定する必要があるので、versionBranchCommit で任意のものを選択して次に進みます。

image.png

リポジトリの Fetch が終わると、ライブラリの Target 選択画面が開くので任意の Target を選択して次に進みます。

image.png

すると下記のようにインストールが確認できるとライブラリが利用可能な状態になります。

image.png

参考

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

MarketingCloudSDK In-App Messaging概要

In-App Messagingとは

ユーザがアプリのプッシュ通知を許可していなくてもユーザにメッセージを配信できるアプリ内メッセージのことです。
アプリがフォアグラウンドの状態になる度にアプリ内メッセージが読み込まれ、SMCで作成したメッセージが、アプリのビュースタックの最上部に表示されます。(閉じたメッセージは再度表示されません)

メッセージのテンプレートは以下3つがあります

フルページ
 レイアウトが画面全体に表示される
バナー
 画面最下部または最上部に表示される
モーダル
 画面の一部に全面表示される

上記3つともSMCのContent Builderで設定可能です。
また色や、画像の配置、フォントサイズなどカスタマイズ可能になっています。
※文字のフォントに関してはデバイスのシステムフォントを使用しているため、アプリ側のフォント上書きの実装が必要です。

通知するにはJourneyBuilderからアプリ内メッセージの送信が必要です。

実装

以下、公式ドキュメントから参照となります。

1.アプリ内メッセージング機能をViewControllerにデリゲートするためにsfmc_setEventDelegateメソッドを使用を使用する

ViewController.swift
// クラス宣言のMarketingCloudSDKEventDelegateプロトコルに従います。
class MyViewController: UIViewController, MarketingCloudSDKEventDelegate

...

// 実装のどこかで、アプリ内メッセージイベントのためのSDKのデリゲートとしてクラスを設定します。

// 可能であれば、クラスの初期化の早い段階で行う必要があります。
MarketingCloudSDK.sharedInstance().sfmc_setEventDelegate(self)

2.ビューの表示や削除に対応するデリゲートメソッドを追加する

ViewController.swift
func sfmc_didShow(inAppMessage message: [AnyHashable : Any]) {
    // message shown
}

func sfmc_didClose(inAppMessage message: [AnyHashable : Any]) {
    // message closed
}

3.アプリ内メッセージの表示を遅延させたり、防止したりすることができる。例えば、ロード中、インストラクション中、サインインフロー中などにアプリ内メッセージが表示されないようにする。メッセージの表示を防ぐには、shouldShowInAppMessageメソッドをfalseを返すようにする。

ViewController.swift
func sfmc_shouldShow(inAppMessage message: [AnyHashable : Any]) -> Bool {
    // メッセージを表示するタイミングなどのロジックを書く
    if (self.messageCanBeShown) {
        return true
    }
    else {
        // メッセージデータを取得するできる
        self.showMessageId = self.sdk.sfmc_messageId(forMessage: message)
    }
    return false
}

4.メッセージデータを取得(sfmc_messageIdForMessage)して、後から表示(sfmc_showInAppMessage)することが可能

ViewController.swift
if (self.showMessageId != nil) {
    MarketingCloudSDK.sharedInstance().sfmc_show(inAppMessage: self.showMessageId)
}

5.アプリ内メッセージのフォントを変更するなら以下のデリゲートメソッドを使用し、デバイスにインストールされているフォントまたはアプリのカスタムフォントの有効なフォント名をSDKに渡す。

ViewController.swift
MarketingCloudSDK.sharedInstance().sfmc_set(inAppMessageFontName: "Zapfino")

参考リンク(公式)

https://salesforce-marketingcloud.github.io/MarketingCloudSDK-iOS/in-app-message/in-app-messaging.html

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

[iOS]Push通知を実装するにあたり、つまずいた点

前書き

iOSでPush通知を実装しようと奮闘していましたが、
プログラミング初心者の私がつまずいた点をいくつか紹介します

サーバーはニフクラを使用しました

以下の記事を参考に作成しました
Swiftでプッシュ通知を送ろう!
NIFCLOUDのgithub

つまずいた点

1,証明書を何度も作り直してしまった

証明書を何度も作り直すことにより、PCが混乱してしまうことがあるので
キーチェーンアクセスから必要のない証明書を削除しましょう

2,APNs用証明書(.p12)の作り方がわからなかった

以下参照
iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について

3,AppleDeveloperのProfileがinvalid(無効)になっていた

これはどうやって解決したのか詳しくは覚えていないが、すでにあるProvisioningProfileを削除した後、再度Profileを作成しProvisioningProfileをダウンロード
スクリーンショット 2020-08-18 15.20.17.png

その後Xcodeで正しいProfileが入っているか確認
スクリーンショット 2020-08-18 15.23.05.png

これは関係あるかわからないがProfileを作成する際のCertificates(証明書)を全てに選択した

4,Capabilityのつけ忘れ

Push Notificationsをつけ忘れるとAppDelegate内の
didRegisterForRemoteNotificationsWithDeviceToken deviceToken
が呼び出されずに端末情報を登録できないので注意
スクリーンショット 2020-08-18 15.26.59.png

5,ドキュメントを読まなかった

きちんと書いてある。読んでクローンや!
NIFCLOUDのgithub

証明書についてはこの記事から
プッシュ通知に必要な証明書の作り方2020

6,UIApplication.registerForRemoteNotifications() must be used from main thread onlyのエラーが出る

訳すと「メインスレッドから使用しろやボケ」とのこと
スクリーンショット 2020-08-18 15.37.23.png

DispatchQueue.main.async(execute: {
  UIApplication.shared.registerForRemoteNotifications()
})

これで解決

7,Build SettingsのSigningの設定

スクリーンショット 2020-08-18 15.42.11.png

8,BundleIdentifierについて

AppleDeveloperで作成したbundleIdentifier(ここではAとする)を作成後、
違うアプリのbundleIdentifierをAと書き換えることは可能

9,なぜAppleDeveloperでデバイスの登録が必要なのか

未公開のアプリがセキュリティーに関係する機能を使用する際は、登録したデバイスのみであるため。
アプリが公開されればデバイス登録は必要ない。

10,コード

最後にコードを載せておく
githubにコードは上がっているため必要ないと思うが一応

AppDelegate.swift
import UIKit
import UserNotifications
import NCMB

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    //********** APIキーの設定 **********
    let applicationkey = "あなたのキー"
    let clientkey      = "あなたのキー"

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //********** SDKの初期化 **********
        NCMB.initialize(applicationKey: applicationkey, clientKey: clientkey)

        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .badge, .sound]) {granted, error in
            if error != nil {
                // エラー時の処理

                return
            }
            if granted {
                // デバイストークンの要求
                DispatchQueue.main.async(execute: {
                  UIApplication.shared.registerForRemoteNotifications()
                })
                // UIApplication.shared.registerForRemoteNotifications()
            }
        }

        return true
    }

    // デバイストークンが取得されたら呼び出されるメソッド
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // 端末情報を扱うNCMBInstallationのインスタンスを作成
        let installation : NCMBInstallation = NCMBInstallation.currentInstallation
        // デバイストークンの設定
        installation.setDeviceTokenFromData(data: deviceToken)
        // 端末情報をデータストアに登録
        installation.saveInBackground {result in
            switch result {
                case .success:
                    // 端末情報の登録に成功した時の処理
                    break
            case let .failure(error):
                    // 端末情報の登録に失敗した時の処理
                    print(error)
                    break
            }
        }

    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }
}

最後に

一度成功すれば、なんて事のない実装だが、エラーが起きた際のデバックが大変だった。
また、証明書あたりを詳しく理解する必要がある。

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

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

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

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

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

今回は変数や関数などの名前を改善していきます。

変な名前を見つける

ViewController

var list:Results<MemoObject>!
var text = [NSAttributedString]()
let realm = try! Realm()
let object = list[indexPath.row]

list・・・Realmのデータを受け取っているけど、何のリストなのかが不明
text・・・NSAttributedStringに変換したデータを格納しているが、何のためのテキストかが不明
realm・・・Realmのインスタンスだが、Realmのどの機能を担っているかが不明
object・・・取得したデータ群から取り出したデータを格納しているが、何のオブジェクトなのかが分からない

AddMemo

class MemoObject: Object {
    @objc dynamic var data: Data!
    @objc dynamic var identifier: String!
}
let picker = UIImagePickerController()
@IBAction func leftButton(_ sender: Any)
let memo: MemoObject = MemoObject()
let data = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {
            let fullString = NSMutableAttributedString(attributedString: memoTextView.attributedText)
            let pickerImage = image.resized(withPercentage: 0.1)!
            let imageWidth = pickerImage.size.width
            let padding: CGFloat = self.view.frame.width / 2
            let scaleFactor = imageWidth / (memoTextView.frame.size.width - padding)
            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: pickerImage.cgImage!, scale: scaleFactor, orientation: pickerImage.imageOrientation)
            let imageString = NSAttributedString(attachment: imageAttachment)
            fullString.append(imageString)
            memoTextView.attributedText = fullString
        }
}

MemoObject・・・Realmに保存するためのモデルですが、
・どのようなオブジェクトなのか
・dataは何のデータを保存するのか
・identifierは何の識別子なのか
が明確でない。

leftButton・・・Realmにデータを保存する関数だが、そもそもNavigationBarの右側のボタンなのでleftは大間違いで、何の処理かが不明
memo・・・Realmに保存するのに使用する変数だが、メモなのはわかるが、何の用途に使うかの情報が足りない
data・・・Data型に変換されたtextViewの値を格納してるが、何のデータか分からない

picker・・・UIImagePickerだが、UIPickerViewなどと混同しそう
fullString・・・textViewの値をMutableAttributedStringに変換してますがAttributedStringだって情報は名前に含める方がいい
pickerImage・・・指定した値に圧縮(今回は10%に)してるが、圧縮後であることが分からず、こちらがピッカーで選択した画像だと勘違いする
imageWidth・・・pickerで選択した画像の幅だが、わざわざ変数にする必要はない
padding・・・textViewに表示する際の隙間だが、何のためのpaddingかが不明
scaleFactor・・・画像の拡大比率だが、Factorが表すのは何なのかが不明、そもそも比率だとぱっと見で判別できない
imageString・・・AttributedStringに変換した画像だが、imageとstringが連結してるだけで情報も役割も感じ得ない

DisplayMemo

var indexPath: Int!
var memo: MemoObject = MemoObject()
var text = NSAttributedString()

indexPath・・・ViewControllerで選択したindexPath.rowが格納されているが、rowを格納してるか分からず、ViewControllerから渡されたってことが明記されてな
memo・・・ViewControllerから渡されたメモのデータを格納しているが、Realmから取得したデータ、保存する用のデータ、などとごちゃ混ぜになって、どういうデータかが分からない
text・・・ViewControllerから渡されたattributedStringを受け取っているが、何の値でどこから受け取ったのかを明記すべき

EditMemo

extension UIImage {
    func resized() -> UIImage
}
@IBAction func gesture()
@IBAction func complete()

resizedは、画像を圧縮するextensionですが、resizedだと過去形or過去分詞を表すので、resizeやresizingの方がいい
gestureは、長押しのジェスチャーで画像を添付する処理を持つが、どういうジェスチャーで何の処理をするのかが分からない
completeは、Realmに保存した編集したデータを保存するが、何が完了して、何をするのかが分からない

リーダブルコードの教え

「名前に情報を含めよ」

情報には、
・扱う値、処理の概要
・扱う値、処理の場所
・扱う値の単位
・時期、タイミング
が挙げられる。

情報を伝える要素として、
・名前の長さ
・接頭辞、接尾辞
・単語、キーワード
・英語の文法(名詞、動詞)
・関連する属性、状態
が挙げられる。

「誤解されない名前とは?」

・とにかく具体的である
・扱う値に応じた単位を用いている
・読み手の勘違いを回避
・Boolを扱う時、疑問形の並びになる
・他の似た処理とどう違うかが明確
・名前から値の中身を想像できる

「長い名前でも問題ない理由」
エディタの単語補完で頭文字を入力すれば、一瞬で呼び出せるから

「1つの単語やキーワードで良い場合とは?」
スコープが小さい場合、なぜなら、使用するor影響する変数や関数が近くにあるから

名前の改善する

ViewController

var memoList:Results<MemoModel>!
var attributedTextArray = [NSAttributedString]()
let realm = try! Realm()
let selectedMemoObject = memoList[indexPath.row]

memoList・・・複数のデータをRealmから取得するので、Listを追加
attributedTextArray・・・取得したデータのAttributedStringを取り出してるので、末尾をArrayに
realm・・・realmInstanceやrealmRefも考えたが、realmに関する変数は他にないので、realmだけで十分だと判断
selectedMemo・・・tableViewで選択されたデータを取り出し、Realmに1度は保存したデータなので、ObjectやDataなどの単語は省いた

AddMemo

class MemoModel: Object {
    @objc dynamic var data: Data!
    @objc dynamic var identifier: String!
}
let imagePicker = UIImagePickerController()
@IBAction func addMemo(_ sender: Any)
let memoObject: MemoModel = MemoModel()
let archivedAttributedText = try! NSKeyedArchiver.archivedData(withRootObject: memoTextView.attributedText!, requiringSecureCoding: false)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let pickerImage = info[.originalImage] as? UIImage {
            let mutAttrMemoText = NSMutableAttributedString(attributedString: memoTextView.attributedText)
            let resizedImage = pickerImage.resizedImage(withPercentage: 0.1)!
            let width = resizedImage.size.width
            let padding: CGFloat = self.view.frame.width / 2
            let scaleRate = width / (memoTextView.frame.size.width - padding)
            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: resizedImage.cgImage!, scale: scaleRate, orientation: resizedImage.imageOrientation)
            let imageAttributedString = NSAttributedString(attachment: imageAttachment)
            mutAttrMemoText.append(imageAttributedString)
            memoTextView.attributedText = mutAttrMemoText
        }
}

MemoModel・・・Realmに保存するためのモデルで、中の変数(data,identifier)はクラス名でメモのモデルだって明示してるので、名前を変更しないことにした。
imagePicker・・・UIImageのピッカーであることを示す。
addMemo・・・Realmにデータを追加するので、saveではなくaddをにした。
memoObject・・・Realmに保存する前の仮のデータなので、属性(Object)を含め、生成中or生成したデータであることを示す
archivedAttributedText・・・Data型としてアーカイブされたことを示す。

selectedImage・・・UIImagePickerで選択されたことを示す
mutAttrMemoText・・・NSMutableAttributedTextをmutAttrに略し、冗長にはせずに短くした
resizedImage・・・圧縮された画像なので、resizeを過去分詞の形にした
width・・・imagePickerControllerの中でしか使わず(スコープが小さい)、画像に使用すると理解できるので、他の情報は省いた
padding・・・widthと同様なので、他の情報は省いた
scaleRate・・・scale(拡大)Rate(割合)の意味を明確に表せる
imageAttachment・・・同じAttachmentでも画像なので、imageを頭につけた
imageAttributedString・・・画像を持つAttributedStringなので、頭にimageをつけた

DisplayMemo

var selectedMemoObject: MemoModel = MemoModel()
var selectedIndexPathRow: Int!
var selectedMemo_attributedText = NSAttributedString()

selectedMemoObject・・・他画面から渡されたデータであること、単体のデータであることから、属性(Object)を追加
selectedIndexPathRow・・・ViewControllerで選択されたことを示し、属性(Row)を含めて、中身が数字であることを明示
selectedMemo_attributedText・・・選択されたメモのattributedTextであることを示す

EditMemo

extension UIImage {
    func resizeImage() -> UIImage
}
@IBAction func attachImageGesture()
@IBAction func updateMemo()

resizeImage・・・圧縮された画像を返すので、Imageを末尾に付けることで返り値であること示す
attachImageGesture・・・画像を添付するジェスチャーであり、textAttachmentを使用してるのでattachを使用
updateMemo・・・Realm上の該当するデータを更新するのでupdateにした

まとめ

本記事を執筆するまでは名前なんて直感で命名してましたが、たかが名前でも、変えるだけで可読性が上がるんだってようやく体感できました。

普通なら、とにかく開発を進めたくて、とりあえず扱うデータや処理に関連する単語をつなげるだけでは、わかりづらい名前になってしまいます。

しかし、
その状況に適した具体的な情報
いつ、何に、何を、何する、などの情報
が加わると、とてもわかりやすくなります。

また、下記の英語の文法を
・名詞
・動詞
・動詞の時期(現在進行形、過去形、未来形、過去分詞形など)
活用すれば、状態や時期などの細かい情報も含めることが可能です。

海外の記事やドキュメントを読んでいくうちに、英語は上達してくので、”英語やコードを読む”を実践し続ければ問題はないでしょう。

次回は、コードの美しさを改善していきます。

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

jsonから取得した文字が文字化けしてしまう

jsonから取得した文字列をUILabelで表示したところ、
シミュレータでは大丈夫なのですが、実機確認したところ文字化けをします。
原因が分からないので、対応方法がわかりません。どなたか教えていただけると助かります。
また、文字化けを起こす時と起こさない時で、文字の区切られる位置が変わります。

シミュレータ
文字列:

支給
あり)

実機
文字列:
支給あり??? ←黒い六角形に白でハテナが3つ出る。

使用フォント:Hiragino-Sans
使用ラベル:attributedText

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

iOS ReplayKitを調べてみた

iOS ReplayKitを調べた内容を纏めてみました。

環境

  • macOS Big Sur 11.0 Beta
  • Xcode 11.6
  • 実機 iOS 13.6

機能概要

ReplayKitの主な流れ

  • ReplayKitはiOS9から追加
  • iOS11からアーキテクチャが変更され、RepalyKit2に
    • iOS11からコントロールセンターの画面収録機能が追加

ReplayKitには、主に2つの機能がある

1. 画面録画

1.1. Screen Recording(iOS9〜)

  • 実行中のアプリを録画し、プレビューコントローラーから写真や共有で保存 スクリーンショット 2020-08-17 22.45.19.png

1.2. In-App Screen Capture(iOS11〜)

  • 実行中のアプリを録画した映像と音声を取得 スクリーンショット 2020-08-17 22.45.36.png

2. Live Broadcasting

2.1. In-App Broadcast(iOS10〜)

  • 実行中アプリの映像と音声を配信

2.2. Broadcast Pairing(iOS11〜)

2.3. iOS System Broadcast(iOS11〜)

  • 実行中アプリ以外のiOS画面も配信可能に
  • コントロールセンターの画面収録からブロードキャスト配信を行う設定が追加 _2020-08-17_22.53.06.png

2.4. System Broadcast Picker(iOS12〜)

  • 自アプリから画面収録画面を表示 スクリーンショット 2020-08-17 22.54.59.png

実装方法

Screen Recording

RPScreenRecorderRPPreviewViewController を使用する

ネットに情報が多いので割愛

In-App Screen Capture

RPScreenRecorder を使用する

ネットに情報が多いので割愛

In-App Broadcast

RPBroadcastActivityViewController を使用する

いまいち使い方はよくわからず

Broadcast Pairing

RPBroadcastActivityViewController を使用する

preferredExtension に Broadcast Setup UI Extension のBundle IDを設定するとリストにSetup UIが表示されるが、いまいち使い方はよくわからず

iOS System Broadcast

  • File → new → Target から Broadcast Upload Extension を追加する
  • コントロールセンター → 画面収録 を長押しすると一覧に追加したExtensionが表示され、ブロードキャストを開始ができる

  • ブロードキャストを開始すると Broadcasst Upload Extension の processSampleBuffer() で Video / Audio Sample を受信する

System Broadcast Picker

  • 自アプリから画面収録画面を表示するには RPSystemBroadcastPickerView を使用する
let broadcastPicker = RPSystemBroadcastPickerView(frame: CGRect(x: 10, y: 50, width: 100, height: 100))

// 追加したBroadcast Upload ExtensionのBundleIDを設定
broadcastPicker.preferredExtension = kUploadExtension
// マイクのオン/オフ表示を設定
broadcastPicker.showsMicrophoneButton = true

// デフォルト表示の二重丸アイコンを文字列に変更
for subview  in broadcastPicker.subviews {
    let b = subview as! UIButton
        b.setImage(nil, for: .normal)
        b.setTitle("画面共有", for: .normal)
        b.setTitleColor(.black, for: .normal)
}

self.view.addSubview(broadcastPicker)

対応ソース

https://github.com/shima-0215/ReplayKitSample

ReplayKit対応アプリの動作

Zoom

ZoomはReplayKitを使用し、画面の共有をおこなう機能を有している

  • コントロールセンターの画面収録を長押しし、zoom を選択し ブロードキャストを開始する をタップするとiOS画面を共有できる(iOS11〜)
  • ミーティング中画面の 共有 から 画面 をタップすると画面収録画面が表示され、zoom を選択し ブロードキャストを開始する をタップするとiOS画面を共有できる(iOS12〜)

    • iOS11の場合、共有 から画面 をタップすると、コントロールセンターからのセットアップ方法の画面が表示される

      https://support.zoom.us/hc/ja/articles/115005890803-iOS-Screen-Sharing

  • ミーティング中以外にコントロールセンターからブロードキャストを開始するとエラーとなる

  • 画面の共有中に、他の端末でブロードキャストを開始されると、共有中の端末はエラーとなり終了する(後勝ちで動作)

その他

  • Skype, Teams, LINE, LINE LIVE も対応してそうだが、動作は未確認。

今後、調べたいこと

参考サイト

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