2017年7月13日木曜日

CollectionViewのCellの中身を更新

CollectionViewのセルをタッチしたら画像を変更するとか、タッチしたセルだけ選択された印のチェックマークをつけたりする方法。

まず、CollectionViewはTableView同様、最初に読み込まれるときにdelegateメソッドで一気に書かれるのが基本。

セルがタッチされたときに呼ばれる以下のdelegateメソッドに記述することで、特定のセルだけを内容変更することができる。
ただし、この中で直接セルの中の部品をいじってもダメなので、ここでは後述するdelegateメソッドの中で変更するように変更内容を入れる配列(ここではcellIsChecked)などをいじるだけとなる。

//セルが選ばれた時に呼ばれるdelegate
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
    //選ばれたセルのcheckBtnのチェックを反転
    if cellIsChecked[indexPath.row] {
        cellIsChecked[indexPath.row] = false
    } else {
        cellIsChecked[indexPath.row] = true
    }
    
    //セルの中身を更新したら、Cellを更新しないと反映されない
    collectionView.reloadItems(at: [indexPath])
    
    return true

}

セルの中身の変更はあくまでdelegateメソッド内で

collectionView.reloadItems(at: [indexPath])を実行することで、以下のdelegateメソッドが順番に呼ばれる。
つまり、あくまでも変更は以下のメソッド中で行うということになる。
ただし、最初に読み込まれる時と違い、下のメソッドはreloadItemsで指定したindexの分だけが呼ばれる。

//セクションの数を指定
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int)

//セルの中身を指定
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
{
    //選ばれたセルをセル番号で取り出す
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! PhotoCollectionViewCell
    var title:String
    if cellIsChecked[indexPath.row] { title = "" } else { title = "" }
    cell.checkBtn.setTitle(title , for: .normal)
        
    return cell

}


collectionView.reloadItems(at: [indexPath])
のかわりに

collectionView.reloadData()
で全体を書き換えることもできるが、処理が重いのでオススメはできない。

2017年7月12日水曜日

Documentsフォルダ内の画像ファイル読み書き

アプリ内にあるDocumentsフォルダに対して、画像ファイルを読み書きする方法。

Swift3ではURL(Swift2.xまではたしかNSURL)型で保存先情報を持つのだが、そのURLをそのまま引数に書いてやってもうまくいかない。
myURLに入っているなら、myURL.pathというプロパティで指定してやる。
引数はString型のようなのでmyURL.descriptionとやってみたがダメだった。

実機で試した場合のpathとdescriptio

ファイルpath
 /var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

ファイルdescription
 file:///var/mobile/Containers/Data/Application/47DCD124-F9A4-4953-9B81-696DA790CCED/Documents/20170711055328142.jpg

頭のfile:///の有無だけのようだ。

書き込み

//Documentsディレクトリのpathを得る(返り値はArrayで、index0がそれ)
let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//StringappendingPathComponentがないのでURLに変換
let fileURL = URL(fileURLWithPath: docPath).appendingPathComponent(fileName)
//JPGに変換
let imageData = UIImageJPEGRepresentation(savePhoto, 1.0)
//画像書き込み(URLのpathを引数に)
//write(to:)はエラーを投げる関数なので、do-catch文が必要
do {
   //do-catchを使ってるので書込みエラーが起きるとcatchに移ってくれる
     try imageData?.write(to: fileURL, options: .atomic)
   //書き込み成功時の処理
   (省略)
   } catch let error {
   //書き込み失敗時の処理
     print("画像保存失敗 \(error)")
   }

読み込み

エラー処理なんかはしてない。

//Documentsディレクトリのpathを得る
let filePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
//その後にファイル名を追加
//(String型のfileWithPathのメソッドがSwiftにないので、一度URL型に変更)
let fileURL = URL(fileURLWithPath: filePath).appendingPathComponent("ファイル名")
//.pathプロパティを引数に画像読み込み
let uiImage = UIImage(contentsOfFile: fileURL.path)

2017年6月7日水曜日

画面キャプチャ方法

//余計なUIをキャプチャしないように隠す
hogeButton.isHidden = true
hogeLabel.isHidden = true
CATransaction.flush() //画面更新
        
//viewと同じサイズのコンテキスト(オフスクリーンバッファ)を作成
let rect = self.view.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
//スクリーンをコンテキストに描画 スクリーンキャプチャ
self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: false)

//スクリーンがキャプチャされたコンテキストからUIImageを作成
let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
//コンテキストの扱いを終了
UIGraphicsEndImageContext()
        
//隠したUIを表示させる
hogeButton.isHidden = false
hogeLabel.isHidden = false

アニメの途中が表示されない問題

Context(オフスクリーンバッファみたいなもん)を作ってそこに画像をdrawしたりrenderしたりしてたんだけど、UIViewアニメの途中をキャプろうとしても、なぜかアニメ中のオブジェクトが実際に表示されている位置でキャプチャされてくれない現象が。

詳しいことはわからんのだが、アニメのメソッドで指定してる最後の移動位置でキャプチャされちゃうみたい。
つまり、AからBの位置に5秒間で移動するアニメを実行中、3秒目でキャプチャしてもなぜかキャプチャ画像だけはBの位置に来てる。(画面上ではまだ途中を動いてるというのに)

renderやdrawじゃなくdrawHierarchy

それを回避するため、drawHierarchy()メソッドを使うのがいいようだ。
ただし、afterScreenUpdatesがtrueの場合は同じような問題が発生するので、falseにしなければいけない。

直前のhiddenが効かない新たな問題

でもfalseにすると、写したくないUI部品をキャプチャ動作の直前でhiddenしても、hiddenされなくて写り込んでしまう。
UI部品をhiddenした直後にsetNeedsDisplay()とかやってもダメ。これは画面更新の予約だけで、実際に更新されるのはOS次第なんだそうな。

よくわからんがflush()で解決

そこで調べたところ、部品をhiddenした直後に CATransaction.flush() をしたところうまくいった。
一瞬部品がhiddenされ、スクリーンキャプチャされた後にまた部品が表示される。
CATransactionはCALayerのアニメを使うためのクラスだそうで、flush()はRunLoop終わりで自動的に呼ばれるものを強制的に(?)実行させるものらしい。
flashじゃなくてflushな。顔を赤らめるとか、興奮、感情の芽生え、トイレの貯水槽とかの意味がある。なんだよそれ!?w
正直言ってUIViewのメソッドじゃないのにうまくいくのは納得いかないんだけど、結果オーライでいいや。

2017年5月18日木曜日

画像サイズ変更

画像サイズを縮小したり拡大したりする方法。とりあえず縮小で説明。

要は、UIGraphicsImageContext(オフスクリーンバッファだな)を縮小(拡大)したサイズで作り、そこに元の画像をdrawすればいい。

以下でimageには512*512の画像が入っているとする。それを256*256のContextにdrawすると、Contextの縦横に合わせて画像が縮小されるわけですな。


//オフスクリーンのコンテキスト作成
UIGraphicsBeginImageContextWithOptions(CGSize(width: 256, height: 256), false, 0.0) 
//コンテキストに画像を描画
image.draw(in: CGRect(x: 0, y: 0, width: 256, height: 256))
//コンテキストから画像を得る
myImageView.image = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()


drawする際にCGRectの各値を変更すれば、描画位置を変えたり、縦横を縮小(拡大)して描画することができる。

単純な拡大も、例えば100*100の画像を256*256のコンテキストに描画すれば拡大されるっちゅうわけ。

画像のクロップ(切り抜き)

画像の一部をクロップする方法。(とりあえず矩形で)

たとえば1024*1024の画像があったとして、そのx:100, y:100, width:200, height:200 の部分をクロップしたい場合、
UIImageのcgImage.cropping(to:)でクロップしたい範囲を指定してやれば、クロップした画像がCGImageとして得られるので、それを再びUIImageに変換してやればいい。
か〜んた〜ん♪


    func cropImage(image:UIImage, cropRect:CGRect) -> UIImage {
        let cropRef   = image.cgImage!.cropping(to: cropRect)
        let cropImage = UIImage(cgImage: cropRef!)
        
        return cropImage
    }

スクリーンショット

スクリーンショットの撮りかた。
GraphicsContext(要はメモリ上に作る描画領域=オフスクリーンバッファだな)を作り、そこに画面に表示されてるviewのlayer(bitmapの画像がある…はず)をrenderしてやることでスクリーンショットが撮れるわけですな。

今回はviewの部分だけのキャプチャーなので、画面一番上に時計とかを表示してるステータスバーなんかはキャプチャーされない。
普通にスクリーンショットを撮った時みたいに、そこまで含めたい場合はまた今度調べる。


    func getScreenShot() -> UIImage {
        //viewと同じサイズのコンテキスト(オフスクリーンバッファ)を作成
        let rect = self.view.bounds
        UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
        //現在のviewの中身(スクリーン)をコンテキストに描画
        //viewの中身なのでステータスバーなどは含まれないようだ
        let context: CGContext = UIGraphicsGetCurrentContext()!
        self.view.layer.render(in: context)
        //スクリーンがキャプチャされたコンテキストからUIImageを作成
        let capturedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        //コンテキストの扱いを終了
        UIGraphicsEndImageContext()
        
        return capturedImage

    }


上にボタンとかを重ねてて、それをキャプチャーしたくない場合は、直前でhiddenして直後にまた表示すればいい。

Retinaディスプレイの場合、たとえばiPadの768*1024のスクリーンショットでも、倍の1536*2048の画像ができるので、それをさらに加工したりするときは注意。


このUIGraphicsBeginImageContext() 〜 UIGraphicsEndImageContext()で囲んだ間でオフスクリーンを操作するやり方はいろいろ使い道がありそうだ。

参考サイト

2017年5月12日金曜日

お上からのメール/Your iOS Distribution will expire in 30 days.

Your iOS Distribution will expire in 30 days. というタイトルで、 以下のことが書かれたメールがアップルデベロッパーから届いた。
This certificate will no longer be valid in 30 days. To create a new certificate, visit Certificates, Identifiers & Profiles in your account.
Certificate: iOS DistributionTeam Name: xxxxxxxxxxxxxxx
To learn more about expired certificates, visit the certificates support page
この証明書は30日間有効ではなくなります。 新しい証明書を作成するには、アカウントの証明書、識別子、およびプロファイルにアクセスしてください。 
翻訳するとこうなった。

リンク先に飛ぶとアップルデベロッパーの iOS Certificates のページが開く。
そこのiOS Distribution(配布)についての方が約一月後に切れるようだ。これなんだったっけ?
iOS Development(開発)の方は11月まで有効。
今年の1月にはアップルデベロッパーに金払って年間登録を更新してるから、それじゃないよね。
実機にインストールしたり、AppStoreで配布したりするのに証明書が必要だったと思うので、それだと思うけど、毎年こんな風に更新だか再作成だかを求められたっけ?

CertificatesがPendingになってるのを開いてみたら、以下のようになってて、デバイスをMacに接続してXcodeでOrganizerウィンドウを開き、「Use for Development」をクリックすると自動的に証明書が生成されるって書いてある。

でも今の状態で接続しても、過去にアップロードしたアプリが出てくるだけで、クリックするようなボタンはない。これが今回のメールの件と関係あるかどうかもよくわからんし。

調べてみると、期限が切れたら「Request Certificate」ってボタンが出るんで、それを押してゴニョゴニョすればいいような?
キーチェーンアクセスを起動してどうのこうのするやり方もあったみたい。
iOS Certificatesの画面で「+」を押して作り直す方法も。

正直よくわからんので、期限が切れるまで様子を見てみるか。

追記:念のため販売中のアプリのうち、プロジェクトのAutomatically manage signingのチェックがされてないのがあったので、そいつをチェックしてみた。
これが今後関係してくれるかはわからん。

追記:何もなかった

とりあえずほっといてみたが、その後何も起こらず開発に支障もないので、良かったみたい。なんだったんだ?

参考サイト

2017年4月19日水曜日

CoreImageのTransitionエフェクト

image1からimage2に画像を遷移するエフェクト。

次の例はSwipeエフェクトだが、
フィルターにCIImageに変換したimage1とimage2を設定
諸パラメータを設定するのはCIFilterと基本的に一緒。
その後、何秒で切り替えるかのdurationを設定して開始したら自動で…と思いきや、それがない。
なんとびっくり、自分でタイマーを使って切り替わり具合をその都度フィルターに設定し、表示しなければいけないようだ。

切り替え度合いがどれくらいかを示すパラメータがkCIInputTimeKeyで設定している値。
値は0〜1の範囲で、0が遷移開始前、1が遷移完了後。
(ただし、1.2くらいにしないと遷移が完了しないことがあるようなので、はっきりとわかってない)
この値をいじることで、エフェクトの途中から開始したり、エフェクトの方向を逆にしたりすることができる。その点は自動で処理されるよりは自由度がある。めんどくさいけど。
なめらかに遷移するなら0.01ずつ、おおざっぱに遷移するなら0.1ずつとか、タイマーで呼ぶメソッド中で値を増やしてやればいい。


//フィルタークラスはインスタンス変数として
let myFilter = CIFilter(name: "CISwipeTransition")
var count = 0.0 //遷移のかかり具合用


//以下はどっか適当なところに書く
let CIImage1 = CIImage(image: UIImage(named: "sample1")!)
let CIImage2 = CIImage(image: UIImage(named: "sample2")!)
        
myFilter!.setValue(10,  forKey: kCIInputWidthKey) //切り替わる部分の幅
myFilter!.setValue(0.6, forKey: kCIInputAngleKey) //角度
myFilter!.setValue(0.0, forKey: kCIInputTimeKey) //遷移のかかり具合開始値
myFilter!.setValue(CIImage1, forKey: kCIInputImageKey//開始前画像
myFilter!.setValue(CIImage2, forKey: kCIInputTargetImageKey//完了後画像
        
//Timerの設定
Timer.scheduledTimer(timeInterval: 0.01,
                     target: self,
                     selector: #selector(ViewController.update(timer:)),
                     userInfo: nil,
                     repeats: true)
        
//フィルター加工後の画像を得てImageViewに表示
let outputImage = UIImage(ciImage: myFilter!.outputImage!)

myImageView.image = outputImage




//こっから遷移の更新
func update(timer: Timer) { //引数が使われてないね?
        
count += 0.01 //遷移のかかり具合
myTime = NSNumber(value: count)
        
//タイマー呼ばれるごとにfilterに値を設定
myFilter!.setValue(myTime, forKey: kCIInputTimeKey)
        
// 現時点でのフィルター加工後の画像を得てImageViewに表示
let outputImage = UIImage(ciImage: myFilter!.outputImage!)
myImageView.image = outputImage
//ここでsetNeedsDisplayを呼んでないことに注目(呼んでも誤動作はしない)
}


パラメータ

  • inputImage
    • CIImage
    • 遷移元の画像
  • inputTargetImage
    • CIImage
    • 遷移先の画像
  • inputExtent
    • CIVector
    • デフォルト値は0,0,300,300
  • inputColor
    • CIColor
    • 不透明色?
    • CIColor().redとか指定してやったらエラーで落ちた。なぜだ?
  • inputTime
    • NSNumber
    • 遷移の経過具合を指定。タイマーなどでその都度変更してやる必要がある。
    • 設定値は0〜1.0の範囲と言われるのだが、1.8とかまで指定しないと遷移が完了しないことがあり、不明。
    • 値を変更してやれば途中から始めたり、逆方向に遷移することもできる。
  • inputAngle
    • NSNumber
    • 遷移の方向を示す角度
    • デフォルトの0で水平
    • 設定値の範囲はよくわかんない
  • inputWidth
    • NSNumber
    • 遷移部分の幅。小さければキリっと切り替わるし、大きければぼんやり切り替わる
    • デフォルト値は300
  • inputOpacity
    • NSNumber
    • 切り替わり部分の不透明値
    • 設定値の範囲は0〜1.0

CATransitionで遷移?

アップルの資料には、UIImageView二つにそれぞれFilterをaddして、CATransitionを使った切り替え方法(こちらはdurationとかが設定できる)も載ってるが、うまくいかなかったので、また機会があったら調べる。

参考資料

2017年4月1日土曜日

vendortax@apple.comからのメール

突然vendortax@apple.comというアドレスからPDFファイル付きのメールが届いた。

アップルのアドレスを騙ったウィルスメールではないかと身構えたが、調べたらアップルからの売り上げと手数料、税金に関する情報だった。
PDFファイルには売上金、コミッション(アップルのみかじめ料w 現在30%)、消費税なんかが書かれてる。

以下、俺宛に届いた英文メールの和訳
JCTっていうのはJapanese Consumption Taxで、日本の消費税のこと)
日本のJCTの詳細については、AppleのiTunes販売代理店への送金に関するアドバイスが添付されています。 日本の販売がある月間のみ、税務アドバイスを受け取ります。
税金に関する質問については、vendortax@apple.comまでご連絡ください。
支払い、銀行取引、およびその他のお問い合わせに関する質問については、iTunes Connectの「お問い合わせ」セクションを使用してチケットを送信してください。
敬具、 
iTunes - ベンダー税の遵守
この電子メールと添付ファイルの情報は、指定された受信者の個人的かつ機密的な使用を目的としたものです。 意図された受信者でない場合は、このメッセージを確認、使用、コピー、転送、または他の方法で配布することはできません。 返信用の電子メールで送信エラーを通知し、システムからすべてのメッセージと添付ファイルのコピーを削除してください。 このメッセージに送信者の名前を使用することは、適用法に基づく電子署名ではありません。 ありがとうございました。

正直、よくわからんのですが、税金を払わなきゃいかんほど売り上げてないので(ノД`)、今回は勘弁しといてやるw

もしいっぱい売り上げてた場合、自分で消費税を申告するのかね?
アップルのコミッション請求に対して消費税8%がかかってると書いてるんで、それを引かれた自分の手取り分に対して処理するのかしら?
いっぱい売り上げてから調べたいと思うw

2017年3月30日木曜日

Xcode8.3で変わったこと(気づいたことだけ)

macOSとiOSがアップデートされ、同時にXcodeも8.3に上がった。
そしたらまたいくらか仕様の変更があった模様。

気づいたことだけまとめる。

M_PIが非推奨に

πのM_PIが非推奨になり、Double.pi.piを使えってWarningが出た。
でも.piに置き換えるとまたエラーになるので、Double.piに置き換えた。

Swift2.3が非サポートに

Swift3.xだけのサポートになったみたい? コードのコンバート機能は残ってると思うのでそいつでどうぞ。あんまり便利じゃないけど。

2017年3月9日木曜日

モーションセンサー、うまくいくようないかないような



モーションセンサーには以下のようなものがある。
値の範囲は決まっているはずだが、センサーの精度の問題なのか、範囲の上限/下限値にならないことも多く、上限/下限値をまたいだ処理をさせると値が飛んでしまうこともあり困る。
プロはどうやってスムーズな処理をしてるんだろう? 誰かおせーて!!
  • 加速度センサー(Accelerometer)
    • X軸、Y軸、Z軸方向に動かした場合、どちらの方向にどれだけ加速度がかかったか
  • ジャイロセンサー(Gyro)
    • X軸、Y軸、Z軸を軸として傾けた場合の、1秒あたりに傾いたラジアン角を得られる(角速度)
  • 姿勢センサー(Attitude)
    • X軸、Y軸、Z軸を軸として傾けた場合の絶対角を得られる。水平な台の上に画面を上にして寝かせて置いた時がそれぞれ0度。
    • X軸中心の前後の傾きがpitch
      • 画面上で寝かせて0度。起こしていくと増えていき、垂直で90度。さらに画面下向きに寝かせていくと減っていき0度に。
      • 増えていった値が90度を境に減るはずだが、時には87度前後で減り出したり、よくわからない。
    • Y軸中心の左右の回転がroll
      • pitchが90度を超えると急に-150度とかになったり、わからない
    • Z軸中心の左右の傾きがyaw
      • これがわずかに傾いてるだけでもpitchの値が90までいかないうちに減り出すことがあるようだ。
  • 重力加速度(Gravity)
    • 地球の重力に対しての向き
    • X軸はLandscapeでホームボタンが右なら-1.0、左なら1.0、Portrateなら0
    • Y軸はPortrateでホームボタンが下なら-1.0、上なら1.0、Landscapeなら0
    • Z軸は画面が真上なら-1.0、垂直に立てると0、画面が真下なら1.0
ライブラリはあらかじめCoreMotionをimportしとく。
import CoreMotion //モーションセンサー

モーションマネージャーはインスタンス変数として作っておかないと、ARCですぐに消されちゃうので注意。
var motionMgr:CMMotionManager?

センサーの設定とスタートまで

//モーションマネージャーは各種センサー共通
motionMgr = CMMotionManager()
//更新頻度(秒)
motionMgr?.deviceMotionUpdateInterval = 0.1 //姿勢センサー
//姿勢センサースタート(クロージャでセンサーが更新された時の処理を書く)
motionMgr?.startDeviceMotionUpdates(to: OperationQueue.current!, withHandler: { (data, error) in
        let pitchDeg = round(180 * data!.attitude.pitch / M_PI)
        let rollDeg = round(180 * data!.attitude.roll / M_PI)
        let yawDeg = round(180 * data!.attitude.yaw / M_PI)

})

//姿勢センサーの停止
motionMgr?.stopDeviceMotionUpdates()

ローパスフィルター

センサーの値がノイズによってばらけることが多いらしく、前回検出した値との加算平均を取る処理(これをローパスフィルターとかハイパスフィルターとか言うらしい)をした方がいいそうである。
最新の値の90%と、前回の値の10%を足すわけですな。

//ローパスフィルター処理(ここでは0.9がレート)
var outputValue:Double
if (self.prevAngle != nil) {
        outputValue = (pitchDeg * 0.9) + (self.prevAngle! * (1.0 - 0.9))
    } else {
        outputValue = pitchDeg
    }
self.prevAngle = outputValue

他のセンサー使用時

センサーの値を取り出す各種プロパティは省略。


//各センサーの更新頻度(秒)設定
motionMgr?.accelerometerUpdateInterval = 0.1 //加速度センサー
motionMgr?.gyroUpdateInterval = 0.1 //ジャイロセンサー

//各センサーのスタート(引数は省略したが姿勢センサーと同様に)

motionMgr?.startAccelerometerUpdates() //加速度センサー

motionMgr?.startGyroUpdates() //ジャイロセンサー



//各センサー停止
motionMgr?.stopAccelerometerUpdates() //加速度センサー
motionMgr?.stopGyroUpdates() //ジャイロセンサー

参考サイト

2017年3月8日水曜日

画像の合成、初歩の初歩

ある画像の上に別の画像を合成したい場合の初歩的な覚書。

UIGraphicsのImageContextを使う。これは要するにオフスクリーン描画領域。画面に表示する前の画像を扱うために確保するメモリ領域だな。
メモリ領域と言ってもwidthとheightのsizeで大きさを指定して作る。

使い方は難しくないけどちょっと変わっていて、Contextを作る
UIGraphicsBeginImageContext(CGSize)
UIGraphicsEndImageContext()
で挟まれた間に処理を書くことになる。

作られたContextは明確な変数に入るわけじゃないようだが、イメージに対してのdrawメソッドや、レイヤーに対してのrenderメソッドなどを書くとContextに対して描画が行われる。
例)
let img = UIImage(named: "ufoImage")
img?.draw(in: CGRect(origin: CGPoint.zero, size: view.frame.size))

上記はimgの画像Contextに描画するというものであり、img描画するわけじゃないからね。

いくつかUIImageを用意して順番にContextにdrawしてやれば、重ねられた合成画像が得られるわけだ。
いろいろ処理した後の画像は
UIGraphicsGetImageFromCurrentImageContext()
関数で得ることができる。(これは引数なし)

実際のコード

//画像の準備
let backImg = UIImage(named: "backImage"//背景画像
let ufoImg = UIImage(named: "ufoImage"//上に合成する画像
//オフスクリーンのContext作る
UIGraphicsBeginImageContext(view.frame.size)
//背景をContextに描画
backImage?.draw(in: CGRect(origin: CGPoint.zero, size: view.frame.size))
//合成する画像を位置を指定して描画
ufoImg?.draw(in: CGRect(x: view.frame.midX, y: view.frame.midY, width: 50, height: 50))
//context上に合成された画像を得る
let compositedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Contextは多用途

このContextを使うのは当然画像合成だけじゃなくいろいろ使える。
UIImageとCGImageなんかを取っ替え引っ替えいじってると画像の傾きがおかしくなったりすることもあるんだけど、そういう時にUIImageやCGImageなどの仕様上の呪縛(制約)から解放した画像を作り、あらためてUIImageを作り直してやるとかにも使える。

でかいサイズの画像を小さいサイズのContextに描画してやるとサイズ縮小ができたりとか。

まだよくわかってないことも多いので、いずれあらためて書くかも。