diff --git a/RACNest.xcodeproj/project.pbxproj b/RACNest.xcodeproj/project.pbxproj index 495ce5f..70d60e9 100644 --- a/RACNest.xcodeproj/project.pbxproj +++ b/RACNest.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B20B7B851EA2EFC100BC5ABA /* RacExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B20B7B841EA2EFC100BC5ABA /* RacExtensions.swift */; }; B25D051B1EA2CCBD0039D323 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B25D051A1EA2CCBD0039D323 /* ReactiveSwift.framework */; }; C72D32E21C470F5300F88B11 /* RACNestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D32E11C470F5300F88B11 /* RACNestTests.swift */; }; C72D32F81C470FE000F88B11 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D32ED1C470FE000F88B11 /* AppDelegate.swift */; }; @@ -15,6 +16,10 @@ C72D32FB1C470FE000F88B11 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C72D32F21C470FE000F88B11 /* Main.storyboard */; }; C72D330D1C471B0C00F88B11 /* CellProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D330C1C471B0C00F88B11 /* CellProtocols.swift */; }; C72D330F1C471B3000F88B11 /* TableViewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D330E1C471B3000F88B11 /* TableViewProtocols.swift */; }; + C741B5A31C5E940400C69372 /* PuzzleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741B5A21C5E940400C69372 /* PuzzleViewController.swift */; }; + C741B5A61C5E96DF00C69372 /* PuzzleBoard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741B5A51C5E96DF00C69372 /* PuzzleBoard.swift */; }; + C741B5A81C5EA01900C69372 /* PuzzlePiece.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741B5A71C5EA01900C69372 /* PuzzlePiece.swift */; }; + C741B5B81C5EC65600C69372 /* PuzzlePieceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C741B5B61C5EC65600C69372 /* PuzzlePieceViewModel.swift */; }; C76C205B1C504B9D0083F4F5 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76C205A1C504B9D0083F4F5 /* SearchViewController.swift */; }; C76C20601C5053660083F4F5 /* words.txt in Resources */ = {isa = PBXBuildFile; fileRef = C76C205F1C5053660083F4F5 /* words.txt */; }; C7852C331C4ACAAA00375089 /* StoryboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7852C321C4ACAAA00375089 /* StoryboardViewController.swift */; }; @@ -27,6 +32,8 @@ C7CB87121C51AB8E00ED9AE6 /* GenericTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CB87111C51AB8E00ED9AE6 /* GenericTableCell.swift */; }; C7CB87161C51AC7000ED9AE6 /* SearchCellItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CB87141C51AC7000ED9AE6 /* SearchCellItem.swift */; }; C7CB87171C51AC7000ED9AE6 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CB87151C51AC7000ED9AE6 /* SearchViewModel.swift */; }; + C7E353681C680DC200A6C9DE /* PuzzleBoardDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E353671C680DC200A6C9DE /* PuzzleBoardDataSource.swift */; }; + C7E3536A1C680E5C00A6C9DE /* PuzzleBoardAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7E353691C680E5C00A6C9DE /* PuzzleBoardAnimator.swift */; }; C7F740501C4B219D00519895 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7F7404D1C4B219D00519895 /* ReactiveCocoa.framework */; }; C7F740511C4B219D00519895 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7F7404E1C4B219D00519895 /* Result.framework */; }; C7F740591C4B2A2F00519895 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F740581C4B2A2F00519895 /* UserDefaults.swift */; }; @@ -43,6 +50,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B20B7B841EA2EFC100BC5ABA /* RacExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RacExtensions.swift; path = ../../../../../Desktop/RacExtensions.swift; sourceTree = ""; }; B25D051A1EA2CCBD0039D323 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/iOS/ReactiveSwift.framework; sourceTree = ""; }; C72D32C91C470F5200F88B11 /* RACNest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RACNest.app; sourceTree = BUILT_PRODUCTS_DIR; }; C72D32DD1C470F5300F88B11 /* RACNestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RACNestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,6 +63,10 @@ C72D32F51C470FE000F88B11 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C72D330C1C471B0C00F88B11 /* CellProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellProtocols.swift; sourceTree = ""; }; C72D330E1C471B3000F88B11 /* TableViewProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewProtocols.swift; sourceTree = ""; }; + C741B5A21C5E940400C69372 /* PuzzleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzleViewController.swift; sourceTree = ""; }; + C741B5A51C5E96DF00C69372 /* PuzzleBoard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzleBoard.swift; sourceTree = ""; }; + C741B5A71C5EA01900C69372 /* PuzzlePiece.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzlePiece.swift; sourceTree = ""; }; + C741B5B61C5EC65600C69372 /* PuzzlePieceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzlePieceViewModel.swift; sourceTree = ""; }; C76C205A1C504B9D0083F4F5 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; C76C205F1C5053660083F4F5 /* words.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = words.txt; sourceTree = ""; }; C7852C321C4ACAAA00375089 /* StoryboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardViewController.swift; sourceTree = ""; }; @@ -67,6 +79,8 @@ C7CB87111C51AB8E00ED9AE6 /* GenericTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericTableCell.swift; sourceTree = ""; }; C7CB87141C51AC7000ED9AE6 /* SearchCellItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchCellItem.swift; sourceTree = ""; }; C7CB87151C51AC7000ED9AE6 /* SearchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + C7E353671C680DC200A6C9DE /* PuzzleBoardDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzleBoardDataSource.swift; sourceTree = ""; }; + C7E353691C680E5C00A6C9DE /* PuzzleBoardAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PuzzleBoardAnimator.swift; sourceTree = ""; }; C7F7404C1C4B219D00519895 /* Rex.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Rex.framework; path = Carthage/Build/iOS/Rex.framework; sourceTree = ""; }; C7F7404D1C4B219D00519895 /* ReactiveCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveCocoa.framework; path = Carthage/Build/iOS/ReactiveCocoa.framework; sourceTree = ""; }; C7F7404E1C4B219D00519895 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = Carthage/Build/iOS/Result.framework; sourceTree = ""; }; @@ -94,6 +108,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B20B7B7F1EA2EF9B00BC5ABA /* RAC Extensions */ = { + isa = PBXGroup; + children = ( + B20B7B841EA2EFC100BC5ABA /* RacExtensions.swift */, + ); + path = "RAC Extensions"; + sourceTree = ""; + }; C72D32C01C470F5200F88B11 = { isa = PBXGroup; children = ( @@ -164,6 +186,7 @@ C72D32F61C470FE000F88B11 /* ViewControllers */ = { isa = PBXGroup; children = ( + C741B5A11C5E93E400C69372 /* Puzzle */, C7852C3B1C4ACF9500375089 /* Form */, C76C20591C504B750083F4F5 /* Search */, C7852C3E1C4ACFE000375089 /* Main */, @@ -174,6 +197,7 @@ C72D330A1C471ADB00F88B11 /* Components */ = { isa = PBXGroup; children = ( + B20B7B7F1EA2EF9B00BC5ABA /* RAC Extensions */, C7F740571C4B2A2700519895 /* UserDefaults */, C72D330B1C471AEE00F88B11 /* UI */, ); @@ -191,6 +215,35 @@ path = UI; sourceTree = ""; }; + C741B5A11C5E93E400C69372 /* Puzzle */ = { + isa = PBXGroup; + children = ( + C741B5A21C5E940400C69372 /* PuzzleViewController.swift */, + C741B5A41C5E96D800C69372 /* Board */, + ); + path = Puzzle; + sourceTree = ""; + }; + C741B5A41C5E96D800C69372 /* Board */ = { + isa = PBXGroup; + children = ( + C741B5A51C5E96DF00C69372 /* PuzzleBoard.swift */, + C741B5A71C5EA01900C69372 /* PuzzlePiece.swift */, + C741B5B41C5EC65600C69372 /* ViewModel */, + ); + path = Board; + sourceTree = ""; + }; + C741B5B41C5EC65600C69372 /* ViewModel */ = { + isa = PBXGroup; + children = ( + C7E353671C680DC200A6C9DE /* PuzzleBoardDataSource.swift */, + C7E353691C680E5C00A6C9DE /* PuzzleBoardAnimator.swift */, + C741B5B61C5EC65600C69372 /* PuzzlePieceViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; C76C20591C504B750083F4F5 /* Search */ = { isa = PBXGroup; children = ( @@ -405,19 +458,26 @@ files = ( C7852C441C4ACFE000375089 /* MainViewModel.swift in Sources */, C72D32F81C470FE000F88B11 /* AppDelegate.swift in Sources */, + B20B7B851EA2EFC100BC5ABA /* RacExtensions.swift in Sources */, C7CB87171C51AC7000ED9AE6 /* SearchViewModel.swift in Sources */, C7CB87161C51AC7000ED9AE6 /* SearchCellItem.swift in Sources */, C7CB87121C51AB8E00ED9AE6 /* GenericTableCell.swift in Sources */, C7852C461C4ACFE000375089 /* MainCellItem.swift in Sources */, + C741B5A81C5EA01900C69372 /* PuzzlePiece.swift in Sources */, C7F740591C4B2A2F00519895 /* UserDefaults.swift in Sources */, + C741B5A31C5E940400C69372 /* PuzzleViewController.swift in Sources */, C7852C331C4ACAAA00375089 /* StoryboardViewController.swift in Sources */, + C7E353681C680DC200A6C9DE /* PuzzleBoardDataSource.swift in Sources */, + C741B5B81C5EC65600C69372 /* PuzzlePieceViewModel.swift in Sources */, C72D330F1C471B3000F88B11 /* TableViewProtocols.swift in Sources */, C76C205B1C504B9D0083F4F5 /* SearchViewController.swift in Sources */, + C741B5A61C5E96DF00C69372 /* PuzzleBoard.swift in Sources */, C72D330D1C471B0C00F88B11 /* CellProtocols.swift in Sources */, C7852C4C1C4B0E2B00375089 /* FormViewModel.swift in Sources */, C7852C351C4ACB6B00375089 /* Storyboard.swift in Sources */, C7852C3D1C4ACF9500375089 /* FormViewController.swift in Sources */, C7852C471C4ACFE000375089 /* MainViewController.swift in Sources */, + C7E3536A1C680E5C00A6C9DE /* PuzzleBoardAnimator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/RACNest/AppRelated/StoryboardViewController.swift b/RACNest/AppRelated/StoryboardViewController.swift index 6480097..e59a7eb 100644 --- a/RACNest/AppRelated/StoryboardViewController.swift +++ b/RACNest/AppRelated/StoryboardViewController.swift @@ -3,4 +3,5 @@ import UIKit enum StoryboardViewController : String, StoryboardViewControllerType { case Form = "FormViewController" case Search = "SearchViewController" + case Puzzle = "PuzzleViewController" } diff --git a/RACNest/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/RACNest/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 36d2c80..eeea76c 100644 --- a/RACNest/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/RACNest/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -59,6 +59,11 @@ "idiom" : "ipad", "size" : "76x76", "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" } ], "info" : { diff --git a/RACNest/Resources/Assets.xcassets/Contents.json b/RACNest/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/RACNest/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/RACNest/Resources/Assets.xcassets/japan_forest.imageset/Contents.json b/RACNest/Resources/Assets.xcassets/japan_forest.imageset/Contents.json new file mode 100644 index 0000000..22523d4 --- /dev/null +++ b/RACNest/Resources/Assets.xcassets/japan_forest.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "japan_forest.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/RACNest/Resources/Assets.xcassets/japan_forest.imageset/japan_forest.jpg b/RACNest/Resources/Assets.xcassets/japan_forest.imageset/japan_forest.jpg new file mode 100644 index 0000000..9edc044 Binary files /dev/null and b/RACNest/Resources/Assets.xcassets/japan_forest.imageset/japan_forest.jpg differ diff --git a/RACNest/Resources/Base.lproj/Main.storyboard b/RACNest/Resources/Base.lproj/Main.storyboard index c3dd791..18bd905 100644 --- a/RACNest/Resources/Base.lproj/Main.storyboard +++ b/RACNest/Resources/Base.lproj/Main.storyboard @@ -123,6 +123,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/RACNest/ViewControllers/Main/Components/MainViewModel.swift b/RACNest/ViewControllers/Main/Components/MainViewModel.swift index 14c83f2..8074214 100644 --- a/RACNest/ViewControllers/Main/Components/MainViewModel.swift +++ b/RACNest/ViewControllers/Main/Components/MainViewModel.swift @@ -8,8 +8,9 @@ final class MainViewModel: NSObject { let item1 = MainCellItem(title: "1. Form 🐥", identifier: .Form) let item2 = MainCellItem(title: "2. Search 🔍", identifier: .Search) + let item3 = MainCellItem(title: "3. Puzzle 🏖", identifier: .Puzzle) - items = [item1, item2] + items = [item1, item2, item3] super.init() } diff --git a/RACNest/ViewControllers/Puzzle/Board/PuzzleBoard.swift b/RACNest/ViewControllers/Puzzle/Board/PuzzleBoard.swift new file mode 100644 index 0000000..d128319 --- /dev/null +++ b/RACNest/ViewControllers/Puzzle/Board/PuzzleBoard.swift @@ -0,0 +1,129 @@ +import UIKit +import ReactiveSwift +import ReactiveCocoa +import Result + +struct PuzzleBoardDimension { + let numberOfRows: Int + let numberOfColumns: Int +} + +final class PuzzleBoard: UIView { + + private let boardDimension: PuzzleBoardDimension + private let puzzlePieceSize: CGSize + private var dataSource: PuzzleBoardDataSource + private var animator: PuzzleBoardAnimator + + var puzzleBoardLinesColor = UIColor.gray + var puzzleBoardBackgroudColor = UIColor.white + + init(boardDimension: PuzzleBoardDimension, image: UIImage, puzzlePieceSize: CGSize = CGSize(width: 100, height: 100)) { + + self.boardDimension = boardDimension + self.puzzlePieceSize = puzzlePieceSize + self.dataSource = PuzzleBoardDataSource(image: image, dimension: boardDimension) + self.animator = PuzzleBoardAnimator(dimension: boardDimension) + + let width = Int(puzzlePieceSize.width) * boardDimension.numberOfRows + let height = Int(puzzlePieceSize.height) * boardDimension.numberOfColumns + + super.init(frame: CGRect(x: 0, y: 0, width: width, height: height)) + + backgroundColor = puzzleBoardBackgroudColor + + bootstrap() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Board bootstrap + + private func bootstrap() { + + let piecesDataSource = dataSource.piecesViewModels + .observe(on: QueueScheduler.main) + .map { (viewModels, skippedPiece) in (viewModels, skippedPiece, self.puzzlePieceSize)} + .flatMap(.latest, transform: addPieces) + .on(completed: { self.defineBoardBoundaries() }) + .flatMap(.latest, transform: animator.movePieceRandomly) + + piecesDataSource.start() + } + + // MARK - Animation + + private func pieceAnimation(pieceSize: CGSize) -> (PuzzlePiece, PuzzlePiecePosition) -> Void { + + return { (puzzlePiece, piecePosition) in + + UIView.animate(withDuration: 0.3) { + + let newX = piecePosition.column * Int(pieceSize.width) + let newY = piecePosition.row * Int(pieceSize.height) + + let newLocation = CGPoint(x: newX, y: newY) + puzzlePiece.frame.origin = newLocation + } + } + } + + // MARK: - Board Construction + + private func addPieces(viewModels: [PuzzlePieceViewModel], skippedPiece: PuzzlePiecePosition, puzzlePieceSize: CGSize) -> SignalProducer<([PuzzlePieceViewModel], PuzzlePiecePosition), NoError> { + + return SignalProducer {o, d in + + viewModels.forEach { viewModel in + + let animation = self.pieceAnimation(pieceSize: self.puzzlePieceSize) + let piece = PuzzlePiece(size: puzzlePieceSize, moveAnimation: animation, viewModel: viewModel) + self.addSubview(piece) + + piece.frame = CGRect(origin: piece.frame.origin, size: puzzlePieceSize) + } + + o.send(value: (viewModels,skippedPiece)) + o.sendCompleted() + } + } + + private func defineBoardBoundaries() { + + defineBorder() + defineSquares() + } + + private func defineBorder() { + + layer.borderColor = puzzleBoardLinesColor.cgColor + layer.borderWidth = 1.0 + } + + private func defineSquares() { + + for i in 0.. Bool { + return lhs.hashValue == rhs.hashValue + } +} + +typealias MovePiece = (PuzzlePiece, PuzzlePiecePosition) -> Void + +final class PuzzlePiece: UIView { + + private let puzzleImageView: UIImageView = UIImageView() + private let viewModel: PuzzlePieceViewModel + private let moveAnimation: MovePiece + + init(size: CGSize, moveAnimation: @escaping MovePiece, viewModel: PuzzlePieceViewModel) { + + self.viewModel = viewModel + self.moveAnimation = moveAnimation + + super.init(frame: CGRect(origin: .zero, size: size)) + + addSubview(puzzleImageView) + self.puzzleImageView.image = viewModel.image + + viewModel.currentPiecePosition.producer.startWithValues { piecePosition in + moveAnimation(self, piecePosition) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func layoutSubviews() { + puzzleImageView.frame = bounds + } +} diff --git a/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardAnimator.swift b/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardAnimator.swift new file mode 100644 index 0000000..0a13951 --- /dev/null +++ b/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardAnimator.swift @@ -0,0 +1,72 @@ +import Result +import ReactiveSwift + +private func newPPP(_ row: Int, _ column: Int) -> PuzzlePiecePosition { + return PuzzlePiecePosition(row, column) +} + +struct PuzzleBoardAnimator { + + private let dimension: PuzzleBoardDimension + + init(dimension: PuzzleBoardDimension) { + self.dimension = dimension + } + + func movePieceRandomly(pieces: [PuzzlePieceViewModel], skippedPosition: PuzzlePiecePosition) -> SignalProducer<([PuzzlePieceViewModel], PuzzlePiecePosition), NoError> { + + return createPieceMovementProducer(pieces: pieces, skippedPosition: skippedPosition) + .chain(times: 50, transformation: createPieceMovementProducer) + } + + private func createPieceMovementProducer(pieces: [PuzzlePieceViewModel], skippedPosition: PuzzlePiecePosition) -> SignalProducer<([PuzzlePieceViewModel], PuzzlePiecePosition), NoError> { + + let positionToBeMovedTo = self.randomPosition(positions: self.candidates(skippedPosition: skippedPosition)) + + let associatedViewModel = pieces.filter { $0.currentPiecePosition.value == positionToBeMovedTo}.first! + let newSkippedPosition = associatedViewModel.currentPiecePosition.value + + associatedViewModel.currentPiecePosition.value = skippedPosition + + return SignalProducer(value: (pieces, newSkippedPosition)) .delay(0.3, on: QueueScheduler.main) + } + + private func randomPosition(positions: [PuzzlePiecePosition]) -> PuzzlePiecePosition { + + let index = Int(arc4random_uniform(UInt32(positions.count))) + return positions[index] + } + + private func candidates(skippedPosition: PuzzlePiecePosition) -> [PuzzlePiecePosition] { + + let maxBoardRow = dimension.numberOfRows - 1 + let maxBoardColumn = dimension.numberOfColumns - 1 + + switch (skippedPosition.row, skippedPosition.column) { + + // top left corner + case (0, 0): + return [newPPP(0,1), newPPP(1,0)] + // top right corner + case (maxBoardRow, 0): + return [newPPP(maxBoardRow - 1, 0), newPPP(maxBoardRow, 1)] + // bottom left corner + case (0, maxBoardColumn) : + return [newPPP(0, maxBoardColumn - 1), newPPP(1, maxBoardColumn)] + // botom right corner + case (maxBoardRow, maxBoardColumn) : + return [newPPP(maxBoardRow - 1, maxBoardColumn), newPPP(maxBoardRow, maxBoardColumn - 1)] + // Top Edge && Bottom Edge + case (let aRow, let aColumn) where aRow == 0 || aRow == maxBoardRow: + let rowOffset = aRow == 0 ? 1 : -1 + return [newPPP(aRow, aColumn - 1), newPPP(aRow, aColumn + 1), newPPP(aRow + rowOffset, aColumn)] + // Left and Right Edge + case (let aRow, let aColumn) where aColumn == 0 || aColumn == maxBoardColumn: + let columnOffset = aColumn == 0 ? 1 : -1 + return [newPPP(aRow - 1, aColumn), newPPP(aRow + 1, aColumn), newPPP(aRow, aColumn + columnOffset)] + // All other cases + case (let aRow, let aColumn): + return [newPPP(aRow - 1, aColumn), newPPP(aRow + 1, aColumn), newPPP(aRow, aColumn + 1), newPPP(aRow, aColumn - 1)] + } + } +} diff --git a/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardDataSource.swift b/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardDataSource.swift new file mode 100644 index 0000000..6537688 --- /dev/null +++ b/RACNest/ViewControllers/Puzzle/Board/ViewModel/PuzzleBoardDataSource.swift @@ -0,0 +1,60 @@ +import UIKit +import ReactiveSwift +import Result + +final class PuzzleBoardDataSource { + + let piecesViewModels: SignalProducer<([PuzzlePieceViewModel], PuzzlePiecePosition), NoError> + + init(image: UIImage, dimension: PuzzleBoardDimension) { + + let scheduler = QueueScheduler(name: "puzzle.backgroundQueue") + let producer = sliceImage(image: image, dimension: dimension).start(on: scheduler) + + let randomPiecePosition = randomPosition(dimension: dimension) + let filter = filterPuzzlePiecePosition(skippedPosition: randomPiecePosition) + + piecesViewModels = producer.filter(filter).map(PuzzlePieceViewModel.init).collect().map { ($0, randomPiecePosition) } + } +} + +private func randomPosition(dimension: PuzzleBoardDimension) -> PuzzlePiecePosition { + + let row = Int(arc4random_uniform(UInt32(dimension.numberOfRows))) + let column = Int(arc4random_uniform(UInt32(dimension.numberOfColumns))) + + return PuzzlePiecePosition(row, column) +} + +private func filterPuzzlePiecePosition(skippedPosition: PuzzlePiecePosition) -> (PuzzlePiecePosition, UIImage) -> Bool { + + return { (position, image) in + return skippedPosition != position + } +} + +private func sliceImage(image: UIImage, dimension: PuzzleBoardDimension) -> SignalProducer<(PuzzlePiecePosition, UIImage), NoError> { + + return SignalProducer {o, d in + + let width = Int(image.size.width) / dimension.numberOfColumns + let height = Int(image.size.height) / dimension.numberOfRows + let imageSize = CGSize(width: width, height: height) + + for row in 0.. + let originalPiecePosition: PuzzlePiecePosition + let image: UIImage + + init(originalPiecePosition: PuzzlePiecePosition, image: UIImage) { + self.originalPiecePosition = originalPiecePosition + self.currentPiecePosition = MutableProperty(originalPiecePosition) + self.image = image + } +} diff --git a/RACNest/ViewControllers/Puzzle/PuzzleViewController.swift b/RACNest/ViewControllers/Puzzle/PuzzleViewController.swift new file mode 100644 index 0000000..cab7666 --- /dev/null +++ b/RACNest/ViewControllers/Puzzle/PuzzleViewController.swift @@ -0,0 +1,18 @@ +import UIKit + +private let dimension = PuzzleBoardDimension(numberOfRows: 3, numberOfColumns: 3) + +final class PuzzleViewController: UIViewController { + + private let board = PuzzleBoard(boardDimension:dimension, image: UIImage(named: "japan_forest")!) + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(board) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + board.center = view.center + } +}