- 投稿日:2020-08-18T23:56:12+09:00
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("失敗") } } } }少しでも参考になりましたら幸いです?
参考
- 投稿日:2020-08-18T19:37:44+09:00
Swift Package Manager とは
概要
Swift Package Manager(以降: SwiftPM) は、Swift コードの配布を管理するためのツールです。また、Swift のビルドシステムと統合されていることにより、依存関係のダウンロード・コンパイルなどが最適化されバイナリ、リソースがプロジェクトで使いやすい形になります。 SwiftPM は既存で存在する依存管理ツールの CocoaPods や Carthage とは違い Apple が OSS とし提供しているツールで、Xcode 9以降(Swift 3.0以降)に含まれています。
ライブラリを導入してみる
SwiftPM はXcode10 以前は
Package.swift
などに依存先を記述して、コマンドラインからのみビルド・実行しかできませんでした。そのため、サーバサイドや CUI アプリケーションなどでしか利用シーンがありませんでした。しかし、Xcode 11 以降からは.xcodeproj
に直接依存先の情報などを記述して導入できるようになったので、iOS アプリケーションでも SwiftPM が利用できるようになりました。つまり、CLI としてライブラリを利用する(SwiftFormatなど) + CLI ライブラリ、サーバサイドアプリなどを作成する以外は基本的には、Package.swift
を編集する必要はないということです。Lottie をインストール
File
>Swift Packages
>Add Package Dependency..
を開きます。Choose Package Repository 画面がでたら lottie-ios のリポジトリ URL を入力して次に進みます。
次にバイナリを指定する必要があるので、
version
・Branch
・Commit
で任意のものを選択して次に進みます。リポジトリの Fetch が終わると、ライブラリの Target 選択画面が開くので任意の Target を選択して次に進みます。
すると下記のようにインストールが確認できるとライブラリが利用可能な状態になります。
参考
- 投稿日:2020-08-18T18:33:21+09:00
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.swiftfunc sfmc_didShow(inAppMessage message: [AnyHashable : Any]) { // message shown } func sfmc_didClose(inAppMessage message: [AnyHashable : Any]) { // message closed }3.アプリ内メッセージの表示を遅延させたり、防止したりすることができる。例えば、ロード中、インストラクション中、サインインフロー中などにアプリ内メッセージが表示されないようにする。メッセージの表示を防ぐには、
shouldShowInAppMessage
メソッドをfalseを返すようにする。ViewController.swiftfunc 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.swiftif (self.showMessageId != nil) { MarketingCloudSDK.sharedInstance().sfmc_show(inAppMessage: self.showMessageId) }5.アプリ内メッセージのフォントを変更するなら以下のデリゲートメソッドを使用し、デバイスにインストールされているフォントまたはアプリのカスタムフォントの有効なフォント名をSDKに渡す。
ViewController.swiftMarketingCloudSDK.sharedInstance().sfmc_set(inAppMessageFontName: "Zapfino")参考リンク(公式)
- 投稿日:2020-08-18T16:01:52+09:00
[iOS]Push通知を実装するにあたり、つまずいた点
前書き
iOSでPush通知を実装しようと奮闘していましたが、
プログラミング初心者の私がつまずいた点をいくつか紹介しますサーバーはニフクラを使用しました
以下の記事を参考に作成しました
Swiftでプッシュ通知を送ろう!
NIFCLOUDのgithubつまずいた点
1,証明書を何度も作り直してしまった
証明書を何度も作り直すことにより、PCが混乱してしまうことがあるので
キーチェーンアクセスから必要のない証明書を削除しましょう2,APNs用証明書(.p12)の作り方がわからなかった
以下参照
iosでプッシュ通知の証明書を.p12形式でexportしようとしたら.p12形式が選択できない問題について3,AppleDeveloperのProfileがinvalid(無効)になっていた
これはどうやって解決したのか詳しくは覚えていないが、すでにあるProvisioningProfileを削除した後、再度Profileを作成しProvisioningProfileをダウンロード
これは関係あるかわからないがProfileを作成する際のCertificates(証明書)を全てに選択した
4,Capabilityのつけ忘れ
Push Notificationsをつけ忘れるとAppDelegate内の
didRegisterForRemoteNotificationsWithDeviceToken deviceToken
が呼び出されずに端末情報を登録できないので注意
5,ドキュメントを読まなかった
きちんと書いてある。読んでクローンや!
NIFCLOUDのgithub証明書についてはこの記事から
プッシュ通知に必要な証明書の作り方20206,UIApplication.registerForRemoteNotifications() must be used from main thread onlyのエラーが出る
DispatchQueue.main.async(execute: { UIApplication.shared.registerForRemoteNotifications() })これで解決
7,Build SettingsのSigningの設定
8,BundleIdentifierについて
AppleDeveloperで作成したbundleIdentifier(ここではAとする)を作成後、
違うアプリのbundleIdentifierをAと書き換えることは可能9,なぜAppleDeveloperでデバイスの登録が必要なのか
未公開のアプリがセキュリティーに関係する機能を使用する際は、登録したデバイスのみであるため。
アプリが公開されればデバイス登録は必要ない。10,コード
最後にコードを載せておく
githubにコードは上がっているため必要ないと思うが一応AppDelegate.swiftimport 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:. } }最後に
一度成功すれば、なんて事のない実装だが、エラーが起きた際のデバックが大変だった。
また、証明書あたりを詳しく理解する必要がある。
- 投稿日:2020-08-18T14:03:10+09:00
リーダブルコードで読みにくいコードを改善していく ~ 名前編
どうも、ねこきち(@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にしたまとめ
本記事を執筆するまでは名前なんて直感で命名してましたが、たかが名前でも、変えるだけで可読性が上がるんだってようやく体感できました。
普通なら、とにかく開発を進めたくて、とりあえず扱うデータや処理に関連する単語をつなげるだけでは、わかりづらい名前になってしまいます。
しかし、
・その状況に適した具体的な情報
・いつ、何に、何を、何する、などの情報
が加わると、とてもわかりやすくなります。また、下記の英語の文法を
・名詞
・動詞
・動詞の時期(現在進行形、過去形、未来形、過去分詞形など)
活用すれば、状態や時期などの細かい情報も含めることが可能です。海外の記事やドキュメントを読んでいくうちに、英語は上達してくので、”英語やコードを読む”を実践し続ければ問題はないでしょう。
次回は、コードの美しさを改善していきます。
- 投稿日:2020-08-18T13:24:18+09:00
jsonから取得した文字が文字化けしてしまう
jsonから取得した文字列をUILabelで表示したところ、
シミュレータでは大丈夫なのですが、実機確認したところ文字化けをします。
原因が分からないので、対応方法がわかりません。どなたか教えていただけると助かります。
また、文字化けを起こす時と起こさない時で、文字の区切られる位置が変わります。シミュレータ
文字列:支給
あり)実機
文字列:
支給あり??? ←黒い六角形に白でハテナが3つ出る。使用フォント:Hiragino-Sans
使用ラベル:attributedText
- 投稿日:2020-08-18T00:12:12+09:00
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〜)
1.2. In-App Screen Capture(iOS11〜)
2. Live Broadcasting
2.1. In-App Broadcast(iOS10〜)
- 実行中アプリの映像と音声を配信
2.2. Broadcast Pairing(iOS11〜)
2.3. iOS System Broadcast(iOS11〜)
2.4. System Broadcast Picker(iOS12〜)
実装方法
Screen Recording
RPScreenRecorder
とRPPreviewViewController
を使用するネットに情報が多いので割愛
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 も対応してそうだが、動作は未確認。
今後、調べたいこと
アプリ本体側で iOS System Broadcast の開始と終了の通知を受け取りたい
Broadcast Upload Extension からアプリ本体側へ録画データを渡したい
- App Groups(UserDefaults)経由で渡せばよい?
- https://github.com/webex/spark-ios-sdk/wiki/Implementation-broadcast-upload-extension
参考サイト