A swift mosaic maker

Mark Lucking
5 min readNov 19, 2017

Continuing this series looking into the possibilities with the new drag and drop methods I put together another simple game, that builds a mosaic of an airdrop’ed file, giving you the opportunity to scamble and reassemble it into the bargain. [you shake the iPad to scramble your image].

To build this you need to start by editing your info.plist adding the code needed for you app to reconize an image intended for it.

<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeIdentifier</key>
<string>ch.cqd.mosaicMaker</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<string>mosaicMaker</string>
</dict>
</dict>
</array>

What this says is when you get a document ending in .mosaicMaker, you need to open this app. Of course this means I need to change the extension of the jpg I want to airdrop, not for purests among you sorry.

That added I need to put this code in the app delegate to do something with the file that comes in. Yes, change the extension back to jpg. Obviously this won’t work if you send it png!

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
var destinationPath:URL!
let documentsDirectoryURL = try! FileManager().url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
if url.pathExtension == “mosaicMaker” {
let documentsDirectoryURL = try! FileManager().url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
do {
destinationPath = documentsDirectoryURL.appendingPathComponent(“mosaic.jpg”)
try FileManager.default.moveItem(at: url, to: destinationPath)
} catch {
print(error)
}
}
let nc = NotificationCenter.default
nc.post(name:Notification.Name(rawValue:”showImage”),
object: nil, userInfo:nil)
return true
}

Beyond that it uses iOS standard notification center to tell the ViewController that it had loaded an image that needs to be displayed.

In the viewDidLoad of the View Controller, we need register to recieve the notifcations and tell it what to do when we get it; in this case run the method showImage.

let nc = NotificationCenter.default
nc.addObserver(forName:Notification.Name(rawValue:”showImage”),
object:nil, queue:nil, using: self.showImage(notification: ))

If you been reading the other articles I wrote you reconize some of the code in the show image, it splits the image into a 4x4 parts, draws a grid to hold them and registers each image with the drag and drop protocol. A couple of important points in this code. Firstly we’re using UIStackViews to manage the layout, so we only need to give X/Y contraints to the initial stackview, we than leave them to manage the layout within the UIStackView implementation. That said we do need to give height and width constraints for the images, since UIImageViews do not have an intrinsic size.

func showImage(notification: Notification) {
let documentsDirectoryURL = try! FileManager().url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let sourcePath = documentsDirectoryURL.appendingPathComponent(“mosaic.jpg”)
let image2C = UIImage(contentsOfFile: sourcePath.path)
let image2R = splitImage(image2D: image2C!)
whiteWin = UIStackView()
whiteWin.axis = UILayoutConstraintAxis.vertical
whiteWin.distribution = UIStackViewDistribution.equalSpacing
whiteWin.alignment = UIStackViewAlignment.center
whiteWin.spacing = 8.0
self.view.addSubview(whiteWin)
var index2C = 0
for oLoop in stride(from:0, to:4, by: 1) {
let SV = UIStackView()
SV.axis = UILayoutConstraintAxis.horizontal
SV.distribution = UIStackViewDistribution.equalSpacing
SV.alignment = UIStackViewAlignment.center
SV.spacing = 8.0
whiteWin.addArrangedSubview(SV)
for _ in stride(from:0, to:4, by: 1) {
let image2S = UIImageView()
image2S.translatesAutoresizingMaskIntoConstraints = false
image2S.widthAnchor.constraint(equalToConstant: 128).isActive = true
image2S.heightAnchor.constraint(equalToConstant: 128).isActive = true
image2S.image = image2R[index2C]
image2S.isUserInteractionEnabled = true
let dragInteraction = UIDragInteraction(delegate: self)
image2S.addInteraction(dragInteraction)
let dropInteraction = UIDropInteraction(delegate: self)
image2S.addInteraction(dropInteraction)
SV.addArrangedSubview(image2S)
image2S.tag = oLoop
image2A.append(image2S)
index2C += 1
}
}
whiteWin.translatesAutoresizingMaskIntoConstraints = false
whiteWin.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
whiteWin.centerYAnchor.constraint(equalTo:
self.view.centerYAnchor).isActive = true
}

The split image method here in the meantime needs to be defined too, with the code to it looking like this.

func splitImage(image2D: UIImage) -> [UIImage] {
var imgImages:[UIImage] = []
let imgWH = image2D.size.width / 4
let imgHT = image2D.size.height / 4
let imgWS = image2D.size.width / 4
let imgHS = image2D.size.height / 4
// need to take precision error into account and reduce the total width by 4 since we got 4 parts for imgCH in stride(from:0, to:Int(image2D.size.height) — 4, by: Int(imgHS)) {
for imgCW in stride(from:0, to:Int(image2D.size.width) — 4, by: Int(imgWS)) {
let imgRect = CGRect(x: imgCW, y: imgCH, width: Int(imgWH), height: Int(imgHT))
let imgChop = image2D.cgImage?.cropping(to:imgRect)
let img2D = UIImage(cgImage: imgChop!)
imgImages.append(img2D)
}
}
return imgImages
}

An import cavet here too, if the image doesn’t break into 4 equal parts, the precision error will result in this loop failing. We fudge this by reducing the size of the image by 4. I don’t know if even that will always work, you test it :)

Just a few more lines of code to complete the picture, forgive the pun.

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
image2M = interaction.view as? UIImageView
sourceIndex = image2M.tag
let image = image2M.image
let provider = NSItemProvider(object: image!)
let item = UIDragItem(itemProvider: provider)
item.localObject = image
return [item]
}
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: UIImage.self) { imageItems in
let images = imageItems as! [UIImage]
let dropLocation:CGPoint = session.location(in: self.view)
for image2X in self.image2A {
let frame = self.view.convert(image2X.frame, from: image2X.superview)
if (frame.contains(dropLocation) ) {
self.objectIndex = image2X.tag
let image2S = image2X.image
image2X.image = images.first
self.image2M.image = image2S
}
}
}
}
func dropInteraction(_ interaction: UIDropInteraction,sessionDidUpdate session: UIDropSession) -> UIDropProposal { return UIDropProposal(operation: .move)
}

The drag and drop methods, obviously you need to add the UIDrag and UIDrop delegates to the class in the meantime so that these get called. An important detail within this code; the co-ordinates of drop view returned to the drag view are from the main self.view co-ordinate system, but our array of images contains co-ordinates from the stackview placements; to bridge the gap we need to convert the co-ordinates of the imageViews to the main self.view too before looking for a match.

And with that you have almost the entire thing, the only missing pieces are the code to scramble the images when you shake the iPad, that looks like this.

override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {if motion == .motionShake {
DispatchQueue.main.async {
var image2B:[UIImage] = []
for image2S in self.image2A {
image2B.append(image2S.image!)
}
var image2C = image2B.shuffled()
for image2S in self.image2A {
image2S.image = image2C.popLast()
}
}
}
}

And the code to shuffle the images too, that looks uses GameKit method and is implemented as an Array extension.

extension Array {
func shuffled(using source: GKRandomSource) -> [Element] {
return (self as NSArray).shuffled(using: source) as! [Element]
}
func shuffled() -> [Element] {
return (self as NSArray).shuffled() as! [Element]
}
}

Tell me what you think? Does all this make sense? A final comment, you can use this app just once :) the save of the jpg will fail the second time, I leave it to the reader to fix the code!

Credit: Need to give credit to Shoo Rayner https://www.youtube.com/watch?v=McyImAXhUDY&lc=z22ry5phfyv5xrcn2acdp431k45z11pkghi1tcmhrodw03c010c&feature=em-comments who’s BB8 I used as the demo in the initial image; and indeed to Star Wars to created BB8 droid.

--

--

Mark Lucking

Coding for 35+ years, enjoying using and learning Swift/iOS development. Writer @ Better Programming, @The StartUp, @Mac O’Clock, Level Up Coding & More