how do we create a UICollectionViewLayout like the SnapChat's stories?
Any ideas please?
I'd like to have a solution without external library.
Based on a precedent answer adapted to your issue:
-(id)initWithSize:(CGSize)size
{
self = [super init];
if (self)
{
_unitSize = CGSizeMake(size.width/2,80);
_cellLayouts = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void)prepareLayout
{
for (NSInteger aSection = 0; aSection < [[self collectionView] numberOfSections]; aSection++)
{
//Create Cells Frames
for (NSInteger aRow = 0; aRow < [[self collectionView] numberOfItemsInSection:aSection]; aRow++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:aRow inSection:aSection];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSUInteger i = aRow%3;
NSUInteger j = aRow/3;
CGFloat offsetY = _unitSize.height*2*j;
CGPoint xPoint;
CGFloat height = 0;
BOOL invert = NO;
if (aRow%6 >= 3) //We need to invert Big cell and small cells => xPoint.x
{
invert = YES;
}
switch (i)
{
case 0:
xPoint = CGPointMake((invert?_unitSize.width:0), offsetY);
height = _unitSize.height;
break;
case 1:
xPoint = CGPointMake((invert?_unitSize.width:0), offsetY+_unitSize.height);
height = _unitSize.height;
break;
case 2:
xPoint = CGPointMake((invert?0:_unitSize.width), offsetY);
height = _unitSize.height*2;
break;
default:
break;
}
CGRect frame = CGRectMake(xPoint.x, xPoint.y, _unitSize.width, height);
[attributes setFrame:frame];
[_cellLayouts setObject:attributes forKey:indexPath];
}
}
}
I set the height of unitSize to 80, but you can use the size of the screen if needed, like _unitSize = CGSizeMake(size.width/2,size.height/4.);.
That render:
Side note: It's up to you to adapt the logic, or do changes, the cell frames calculation may not be the "best looking piece of code".
UICollectionViewLayout like the SnapChat's stories Like
Swift 3.2 Code
import Foundation
import UIKit
class StoryTwoColumnsLayout : UICollectionViewLayout {
fileprivate var cache = [IndexPath: UICollectionViewLayoutAttributes]()
fileprivate var cellPadding: CGFloat = 4
fileprivate var contentHeight: CGFloat = 0
var oldBound: CGRect!
let numberOfColumns:Int = 2
var cellHeight:CGFloat = 255
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
contentHeight = 0
cache.removeAll(keepingCapacity: true)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
if collectionView.numberOfSections == 0 {
return
}
let cellWidth = contentWidth / CGFloat(numberOfColumns)
cellHeight = cellWidth / 720 * 1220
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * cellWidth)
}
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
var newheight = cellHeight
if column == 0 {
newheight = ((yOffset[column + 1] - yOffset[column]) > cellHeight * 0.3) ? cellHeight : (cellHeight * 0.90)
}
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: cellWidth, height: newheight)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache[indexPath] = (attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + newheight
if column >= (numberOfColumns - 1) {
column = 0
} else {
column = column + 1
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
visibleLayoutAttributes = cache.values.filter({ (attributes) -> Bool in
return attributes.frame.intersects(rect)
})
print(visibleLayoutAttributes)
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// print(cache[indexPath.item])
return cache[indexPath]
}
}
Related
I try to make an endless background through the nodes, but the background has not become infinite and is interrupted, the third background is not yet shown. After the first show, the number of nodes in the scene grows, how can this be fixed?
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var bgNode: SKNode!
var overlay: SKNode!
var overlayWidth: CGFloat!
//var viewSize: CGSize!
var levelPositionX: CGFloat = 0.0
//var speed: CGFloat = 5.5
override func didMove(to view: SKView) {
setupNode()
//viewSize = CGSize(width: frame.size.width, height:
frame.size.height )
}
func setupNode() {
let worldNode = childNode(withName: "World")!
bgNode = worldNode.childNode(withName: "Background")!
overlay = bgNode.childNode(withName: "Overlay")!.copy() as!
SKNode
overlayWidth = overlay.calculateAccumulatedFrame().width
}
func createBackgroundOverlay() {
let backgroundOverlay = overlay.copy() as! SKNode
backgroundOverlay.position = CGPoint(x: 0.0, y: 0.0)
bgNode.addChild(backgroundOverlay)
levelPositionX += overlayWidth
}
func update() {
bgNode.position.x -= 5
if bgNode.position.x <= -self.frame.size.width {
bgNode.position.x = self.frame.size.width * 2
createBackgroundOverlay()
}
}
override func update(_ currentTime: TimeInterval) {
update()
}
In my endless runner game, I have implemented an endless background and a ground(or floor) much similar to your app. Below I shall discuss the steps i have used in my game.
Step 1: In your GameScene.swift file add these variables.
var backgroundSpeed: CGFloat = 80.0 // speed may vary as you like
var deltaTime: TimeInterval = 0
var lastUpdateTimeInterval: TimeInterval = 0
Step 2: In GameScene file, make setUpBackgrouds method as follows
func setUpBackgrounds() {
//add background
for i in 0..<3 {
// add backgrounds, my images were namely, bg-0.png, bg-1.png, bg-2.png
let background = SKSpriteNode(imageNamed: "bg-\(i).png")
background.anchorPoint = CGPoint.zero
background.position = CGPoint(x: CGFloat(i) * size.width, y: 0.0)
background.size = self.size
background.zPosition = -5
background.name = "Background"
self.addChild(background)
}
for i in 0..<3 {
// I have used one ground image, you can use 3
let ground = SKSpriteNode(imageNamed: "Screen.png")
ground.anchorPoint = CGPoint(x: 0, y: 0)
ground.size = CGSize(width: self.size.width, height: ground.size.height)
ground.position = CGPoint(x: CGFloat(i) * size.width, y: 0)
ground.zPosition = 1
ground.name = "ground"
self.addChild(ground)
}
}
Step 3: Now we have to capture timeIntervals from update method
override func update(_ currentTime: TimeInterval) {
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
}
Step 4: Here comes the most important part, moving our backgrounds and groungFloor by enumerating child nodes. Add these two methods in GameScene.swift file.
func updateBackground() {
self.enumerateChildNodes(withName: "Background") { (node, stop) in
if let back = node as? SKSpriteNode {
let move = CGPoint(x: -self.backgroundSpeed * CGFloat(self.deltaTime), y: 0)
back.position += move
if back.position.x < -back.size.width {
back.position += CGPoint(x: back.size.width * CGFloat(3), y: 0)
}
}
}
}
func updateGroundMovement() {
self.enumerateChildNodes(withName: "ground") { (node, stop) in
if let back = node as? SKSpriteNode {
let move = CGPoint(x: -self.backgroundSpeed * CGFloat(self.deltaTime), y: 0)
back.position += move
if back.position.x < -back.size.width {
back.position += CGPoint(x: back.size.width * CGFloat(3), y: 0)
}
}
}
}
Step 5: At this point you should get this error:"Binary operator '+=' cannot be applied to two 'CGPoint' operands" in updateBackground and updateGroundMovement methods.
Now we need to implement operator overloading to resolve this problem. Create a new Swift File and name it Extensions.swift and then implement as follows:
// Extensions.swift
import CoreGraphics
import SpriteKit
public func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
public func += (left: inout CGPoint, right: CGPoint) {
left = left + right
}
Step 6: call setUpBackgrounds method in didMove(toView:)
override func didMove(to view: SKView) {
setUpBackgrounds()
}
Step 7: Finally call the updateBackground and updateGroundMovement methods in update(_ currentTime) method. updated code is given below:
override func update(_ currentTime: TimeInterval) {
if lastUpdateTimeInterval == 0 {
lastUpdateTimeInterval = currentTime
}
deltaTime = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
//MARK:- Last step:- add these methods here
updateBackground()
updateGroundMovement()
}
I want to make a game when it's game over, a banner ad shows up, but I found out that the view takes at least one minute to load. I tried doing this on a different thread but it didn't work. I created the view in GameViewController.swift, and added the subview in the GameScene.swift. Also the Game Over Pop up is a set of SKSpriteNodes and SKLabelNodes.
GameViewController.swift
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("READY__$$$$$$$$$$$$$$$$$$$_________________.")
banner = GADBannerView(adSize: kGADAdSizeFullBanner)
banner.adUnitID = "ca-app-pub-3940256099942544/2934735716"
let request = GADRequest()
banner.load(request)
banner.frame = CGRect(x: 0, y: (view?.bounds.height)! - banner.frame.size.height, width: banner.frame.size.width, height: banner.frame.size.height)
banner.rootViewController = self
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "IntroScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return true
}
}
GameScene.swift
func spawnAd() {
print("READY___________________.")
DispatchQueue.main.async(execute: {
self.view?.addSubview(banner)
})
// DispatchQueue.global(qos: .userInteractive).async {
// DispatchQueue.main.async {
// self.view?.addSubview(banner)
// }
//
// }
}
The Game Over Screen Pop up
func spawnGameOverMenu() {
let dimPanel = SKSpriteNode(color: UIColor.brown, size: self.size)
dimPanel.alpha = 0.0
dimPanel.zPosition = 9
dimPanel.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
self.addChild(dimPanel)
let fadeAlpha = SKAction.fadeAlpha(by: 0.5, duration: 0.6)
dimPanel.run(fadeAlpha)
gameOverMenu = SKSpriteNode(color: bgColor, size: CGSize(width: self.frame.width - 5, height: self.frame.height / 2))
gameOverMenu.position = CGPoint(x: self.frame.midX, y: self.frame.minY - self.frame.height)
gameOverMenu.name = "gameOverMenu"
gameOverMenu.zPosition = 10
self.addChild(gameOverMenu)
spawnGameOverLabel()
spawnResetLbl()
spawnAd()
let moveUp = SKAction.moveTo(y: self.frame.midY, duration: 1.0)
gameOverMenu.run(moveUp)
}
I figured out why it wasn't working, I set the root view controller after I loaded the request.
banner = GADBannerView(adSize: kGADAdSizeFullBanner)
banner.adUnitID = "ca-app-pub-3940256099942544/2934735716"
banner.rootViewController = self
let request = GADRequest()
banner.load(request)
banner.frame = CGRect(x: 0, y: (view?.bounds.height)! - banner.frame.size.height, width: banner.frame.size.width, height: banner.frame.size.height)
I beginner learning Swift 3 - UISwipe gesture but cannot work if using Array.
This my code.
How can I code so the Label only change from "hello1" to "hello2" then swipe left again to "hello3". Also reverse swipe to right back from "hello3" to "hello2" and "hello1". or loop back to first one.
Thanks.
class ChangeLabelViewController: UIViewController {
var helloArray = ["Hello1", "Hello2", "Hello3"]
var currentArrayIndex = 0
#IBOutlet weak var helloLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(ChangeLabelViewController.handleSwipes(sender:)))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(ChangeLabelViewController.handleSwipes(sender:)))
leftSwipe.direction = .left
rightSwipe.direction = .right
view.addGestureRecognizer(leftSwipe)
view.addGestureRecognizer(rightSwipe)
helloLabel.text = helloArray[currentArrayIndex]
}
func handleSwipes(sender: UISwipeGestureRecognizer) {
if sender.direction == .left {
helloLabel.text = helloArray[currentArrayIndex + 1]
}
if sender.direction == .right {
}
}
Try this:
if sender.direction == .left {
currentArrayIndex = (currentArrayIndex + 1) % 3
helloLabel.text = helloArray[currentArrayIndex]
}
if sender.direction == .right {
currentArrayIndex = (currentArrayIndex + 3 - 1) % 3 //if uInt
helloLabel.text = helloArray[currentArrayIndex]
}
you can also do it with below code,
func handleSwipes(sender: UISwipeGestureRecognizer) {
if sender.direction == .left {
if(currentArrayIndex < helloArray.count - 1)
{
currentArrayIndex += 1
let indexPath = IndexPath(item: currentArrayIndex, section: 0)
helloLabel.text = helloArray[indexPath.row]
}
}
if sender.direction == .right {
if(currentArrayIndex > 0)
{
currentArrayIndex -= 1
if currentArrayIndex == -1
{
currentArrayIndex = 0
let indexPath = IndexPath(item: currentArrayIndex, section: 0)
helloLabel.text = helloArray[indexPath.row]
}
else {
let indexPath = IndexPath(item: currentArrayIndex, section: 0)
helloLabel.text = helloArray[indexPath.row]
}
}
}
I followed this Custom Collection View Layout tutorial from raywenderlich.com using xcode 8 and swift 3.
When I ran the app after implementing all methods requested in the tutorial, I got the following error:
'no UICollectionViewLayoutAttributes instance for -layoutAttributesForItemAtIndexPath: {length = 2, path = 0 - 11}'
So I have added the following method into my CollectionViewFlowLayout class:
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.cache[indexPath.row]
}
This is almost working except that some cells are overlaying existing cells when scrolling down and then disappear. If I scroll up, everything is working perfectly.
I don't understand the full logic yet of this code but I have reviewed and tested it several times and I cannot understand which part of the code is triggering this behaviour.
Any idea?
import UIKit
/* The heights are declared as constants outside of the class so they can be easily referenced elsewhere */
struct UltravisualLayoutConstants {
struct Cell {
/* The height of the non-featured cell */
static let standardHeight: CGFloat = 100
/* The height of the first visible cell */
static let featuredHeight: CGFloat = 280
}
}
class UltravisualLayout: UICollectionViewLayout {
// MARK: Properties and Variables
/* The amount the user needs to scroll before the featured cell changes */
let dragOffset: CGFloat = 180.0
var cache = [UICollectionViewLayoutAttributes]()
/* Returns the item index of the currently featured cell */
var featuredItemIndex: Int {
get {
/* Use max to make sure the featureItemIndex is never < 0 */
return max(0, Int(collectionView!.contentOffset.y / dragOffset))
}
}
/* Returns a value between 0 and 1 that represents how close the next cell is to becoming the featured cell */
var nextItemPercentageOffset: CGFloat {
get {
return (collectionView!.contentOffset.y / dragOffset) - CGFloat(featuredItemIndex)
}
}
/* Returns the width of the collection view */
var width: CGFloat {
get {
return collectionView!.bounds.width
}
}
/* Returns the height of the collection view */
var height: CGFloat {
get {
return collectionView!.bounds.height
}
}
/* Returns the number of items in the collection view */
var numberOfItems: Int {
get {
return collectionView!.numberOfItems(inSection: 0)
}
}
// MARK: UICollectionViewLayout
/* Return the size of all the content in the collection view */
override var collectionViewContentSize : CGSize {
let contentHeight = (CGFloat(numberOfItems) * dragOffset) + (height - dragOffset)
return CGSize(width: width, height: contentHeight)
}
override func prepare() {
let standardHeight = UltravisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltravisualLayoutConstants.Cell.featuredHeight
var frame = CGRect.zero
var y:CGFloat = 0
for item in 0 ... (numberOfItems - 1) {
let indexPath = NSIndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.zIndex = item
var height = standardHeight
if indexPath.item == featuredItemIndex {
let yOffset = standardHeight * nextItemPercentageOffset
y = self.collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if item == (featuredItemIndex + 1) && item != numberOfItems {
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 0)
y = maxY - height
}
frame = CGRect(x: 0, y: y, width: width, height: height)
attributes.frame = frame
cache.append(attributes)
y = frame.maxY
}
}
/* Return all attributes in the cache whose frame intersects with the rect passed to the method */
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.cache[indexPath.row]
}
/* Return true so that the layout is continuously invalidated as the user scrolls */
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Just added that in the super.viewDidLoad of the view controller owning the UICollection view and it is now working as expected.
This is a quick fix and I believe there are a more appropriate way to fix this managing better the prefetching functions from IOS10.
if #available(iOS 10.0, *) {
collectionView!.prefetchingEnabled = false
}
i have a Problem with the method didBeginContact. I have a ball and a wall.
The collision of both objects works fine. If both objects collide, they change their position but the method didBeginContact is not called.
// constants.swift
...
let ballCategory: UInt32 = 0x1 << 0
let wallCategory: UInt32 = 0x1 << 1
...
// Physics balls (ball.swift)
...
init() {
let size = CGSize(width: 32, height: 44)
loadPhysicsBodyWithSize(size: size)
}
func loadPhysicsBodyWithSize(size: CGSize){
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.categoryBitMask = ballCategory
physicsBody?.contactTestBitMask = wallCategory
physicsBody?.affectedByGravity = false
}
...
// Physics wall (wall.swift)
...
init() {
let size = CGSize(width: 32, height: 44)
loadPhysicsBodyWithSize(size: size)
}
func loadPhysicsBodyWithSize(size: CGSize){
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody?.categoryBitMask = wallCategory
physicsBody?.affectedByGravity = false
}
...
// GameScene (gameScene.swift)
class GameScene: SKScene, SKPhysicsContactDelegate {
...
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
}
func didBeginContact(contact: SKPhysicsContact){
print("didBeginContact called")
}
}
Has anybody any idea? Thanks for help!
I solved this by changing
func didBeginContact(contact: SKPhysicsContact)
to
func didBegin(_ contact: SKPhysicsContact)