Related
I am finding my first SwiftUI challenge to be a tricky one. Given a set of playing cards, display them in a way that allows the user to see the full deck while using space efficiently. Here's a simplified example:
In this case 52 cards (Views) are presented, in order of 01 - 52. They are dynamically packed into the parent view such that there is enough spacing between them to allow the numbers to be visible.
The problem
If we change the shape of the window, the packing algorithm will pack them (correctly) into a different number of rows & columns. However, when the number of rows/columns change, the card Views are out of order (some are duplicated):
In the image above, notice how the top row is correct (01 - 26) but the second row starts at 12 and ends at 52. I expect his is because the second row originally contained 12 - 22 and those views were not updated.
Additional criteria: The number of cards and the order of those cards can change at runtime. Also, this app must be able to be run on Mac, where the window size can be dynamically adjusted to any shape (within reason.)
I understand that when using ForEach for indexing, one must use a constant but I must loop through a series of rows and columns, each of which can change. I have tried adding id: \.self, but this did not solve the problem. I ended up looping through the maximum possible number of rows/columns (to keep the loop constant) and simply skipped the indices that I didn't want. This is clearly wrong.
The other alternative would be to use arrays of Identifiable structures. I tried this, but wasn't able to figure out how to organize the data flow. Also, since the packing is dependent on the size of the parent View it would seem that the packing must be done inside the parent. How can the parent generate the data needed to fulfill the deterministic requirements of SwiftUI?
I'm willing to do the work to get this working, any help understanding how I should proceed would be greatly appreciated.
The code below is a fully working, simplified version. Sorry if it's still a bit large. I'm guessing the problem revolves around the use of the two ForEach loops (which are, admittedly, a bit janky.)
import SwiftUI
// This is a hacked together simplfied view of a card that meets all requirements for demonstration purposes
struct CardView: View {
public static let kVerticalCornerExposureRatio: CGFloat = 0.237
public static let kPhysicalAspect: CGFloat = 63.5 / 88.9
#State var faceCode: String
func bgColor(_ faceCode: String) -> Color {
let ascii = Character(String(faceCode.suffix(1))).asciiValue!
let r = (CGFloat(ascii) / 3).truncatingRemainder(dividingBy: 0.7)
let g = (CGFloat(ascii) / 17).truncatingRemainder(dividingBy: 0.9)
let b = (CGFloat(ascii) / 23).truncatingRemainder(dividingBy: 0.6)
return Color(.sRGB, red: r, green: g, blue: b, opacity: 1)
}
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 10)
.fill(bgColor(faceCode))
.cornerRadius(8)
.frame(width: geometry.size.height * CardView.kPhysicalAspect, height: geometry.size.height)
.aspectRatio(CardView.kPhysicalAspect, contentMode: .fit)
.overlay(Text(faceCode)
.font(.system(size: geometry.size.height * 0.1))
.padding(5)
, alignment: .topLeading)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(lineWidth: 2))
}
}
}
// A single rows of our fanned out cards
struct RowView: View {
var cards: [String]
var width: CGFloat
var height: CGFloat
var start: Int
var columns: Int
var cardWidth: CGFloat {
return height * CardView.kPhysicalAspect
}
var cardSpacing: CGFloat {
return (width - cardWidth) / CGFloat(columns - 1)
}
var body: some View {
HStack(spacing: 0) {
// Visit all cards, but only add the ones that are within the range defined by start/columns
ForEach(0 ..< cards.count) { index in
if index < columns && start + index < cards.count {
HStack(spacing: 0) {
CardView(faceCode: cards[start + index])
.frame(width: cardWidth, height: height)
}
.frame(width: cardSpacing, alignment: .leading)
}
}
}
}
}
struct ContentView: View {
#State var cards: [String]
#State var fanned: Bool = true
// Generates the number of rows/columns that meets our rectangle-packing criteria
func pack(area: CGSize, count: Int) -> (rows: Int, cols: Int) {
let areaAspect = area.width / area.height
let exposureAspect = 1 - CardView.kVerticalCornerExposureRatio
let aspect = areaAspect / CardView.kPhysicalAspect * exposureAspect
var rows = Int(ceil(sqrt(Double(count)) / aspect))
let cols = count / rows + (count % rows > 0 ? 1 : 0)
while cols * (rows - 1) >= count { rows -= 1 }
return (rows, cols)
}
// Calculate the height of a card such that a series of rows overlap without covering the corner pips
func cardHeight(frameHeight: CGFloat, rows: Int) -> CGFloat {
let partials = CGFloat(rows - 1) * CardView.kVerticalCornerExposureRatio + 1
return frameHeight / partials
}
var body: some View {
VStack {
GeometryReader { geometry in
let w = geometry.size.width
let h = geometry.size.height
if w > 0 && h > 0 { // using `geometry.size != .zero` crashes the preview :(
let (rows, cols) = pack(area: geometry.size, count: cards.count)
let cardHeight = cardHeight(frameHeight: h, rows: rows)
let rowSpacing = cardHeight * CardView.kVerticalCornerExposureRatio
VStack(spacing: 0) {
// Visit all cards as if the layout is one row per card and simply skip the rows
// we're not interested in. If I make this `0 ..< rows` - it doesn't work at all
ForEach(0 ..< cards.count) { row in
if row < rows {
RowView(cards: cards, width: w, height: cardHeight, start: row * cols, columns: cols)
.frame(width: w, height: rowSpacing, alignment: .topLeading)
}
}
}
.frame(width: w, height: 100, alignment: .topLeading)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(cards: ["01", "02", "03", "04", "05", "06", "07", "08", "09",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
"50", "51", "52"])
.background(Color.white)
.preferredColorScheme(.light)
}
}
I think you're on the right track that you need to use an Identifiable to prevent the system from making assumptions about what can be recycled in the ForEach. To that end, I've created a Card:
struct Card : Identifiable {
let id = UUID()
var title : String
}
Within the RowView, this is trivial to use:
struct RowView: View {
var cards: [Card]
var width: CGFloat
var height: CGFloat
var columns: Int
var cardWidth: CGFloat {
return height * CardView.kPhysicalAspect
}
var cardSpacing: CGFloat {
return (width - cardWidth) / CGFloat(columns - 1)
}
var body: some View {
HStack(spacing: 0) {
// Visit all cards, but only add the ones that are within the range defined by start/columns
ForEach(cards) { card in
HStack(spacing: 0) {
CardView(faceCode: card.title)
.frame(width: cardWidth, height: height)
}
.frame(width: cardSpacing, alignment: .leading)
}
}
}
}
In the ContentView, things get a little more complicated because of the dynamic rows:
struct ContentView: View {
#State var cards: [Card] = (1..<53).map { Card(title: "\($0)") }
#State var fanned: Bool = true
// Generates the number of rows/columns that meets our rectangle-packing criteria
func pack(area: CGSize, count: Int) -> (rows: Int, cols: Int) {
let areaAspect = area.width / area.height
let exposureAspect = 1 - CardView.kVerticalCornerExposureRatio
let aspect = areaAspect / CardView.kPhysicalAspect * exposureAspect
var rows = Int(ceil(sqrt(Double(count)) / aspect))
let cols = count / rows + (count % rows > 0 ? 1 : 0)
while cols * (rows - 1) >= count { rows -= 1 }
return (rows, cols)
}
// Calculate the height of a card such that a series of rows overlap without covering the corner pips
func cardHeight(frameHeight: CGFloat, rows: Int) -> CGFloat {
let partials = CGFloat(rows - 1) * CardView.kVerticalCornerExposureRatio + 1
return frameHeight / partials
}
var body: some View {
VStack {
GeometryReader { geometry in
let w = geometry.size.width
let h = geometry.size.height
if w > 0 && h > 0 { // using `geometry.size != .zero` crashes the preview :(
let (rows, cols) = pack(area: geometry.size, count: cards.count)
let cardHeight = cardHeight(frameHeight: h, rows: rows)
let rowSpacing = cardHeight * CardView.kVerticalCornerExposureRatio
VStack(spacing: 0) {
ForEach(Array(cards.enumerated()), id: \.1.id) { (index, card) in
let row = index / cols
if index % cols == 0 {
let rangeMin = min(cards.count, row * cols)
let rangeMax = min(cards.count, rangeMin + cols)
RowView(cards: Array(cards[rangeMin..<rangeMax]), width: w, height: cardHeight, columns: cols)
.frame(width: w, height: rowSpacing, alignment: .topLeading)
}
}
}
.frame(width: w, height: 100, alignment: .topLeading)
}
}
}
}
}
This loops through all of the cards and uses the unique IDs. Then, there's some logic to use the index to determine what row the loop is on and if it is the beginning of the loop (and thus should render the row). Finally, it sends just a subset of the cards to the RowView.
Note: you can look at Swift Algorithms for a more efficient method than enumerated. See indexed: https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md
#jnpdx has provided a valid answer that is direct in its approach, which helps to understand the problem without adding additional complexity.
I have also stumbled across an alternative approach that requires more drastic changes to the structure of the code, but is more performant while also leading to more production-ready code.
To begin with, I created a CardData struct that implements the ObservableObject protocol. This includes the code to pack a set of cards into rows/columns based on a given CGSize.
class CardData: ObservableObject {
var cards = [[String]]()
var hasData: Bool {
return cards.count > 0 && cards[0].count > 0
}
func layout(cards: [String], size: CGSize) -> CardData {
// ...
// Populate `cards` with packed rows/columns
// ...
return self
}
}
This would only work if the layout code could know the frame size for which it was packing. To that end, I used .onChange(of:perform:) to track changes to the geometry itself:
.onChange(of: geometry.size, perform: { size in
cards.layout(cards: cardStrings, size: size)
})
This greatly simplifies the ContentView:
var body: some View {
VStack {
GeometryReader { geometry in
let cardHeight = cardHeight(frameHeight: geometry.size.height, rows: cards.rows)
let rowSpacing = cardHeight * CardView.kVerticalCornerExposureRatio
VStack(spacing: 0) {
ForEach(cards.cards, id: \.self) { row in
RowView(cards: row, width: geometry.size.width, height: cardHeight)
.frame(width: geometry.size.width, height: rowSpacing, alignment: .topLeading)
}
}
.frame(width: geometry.size.width, height: 100, alignment: .topLeading)
.onChange(of: geometry.size, perform: { size in
_ = cards.layout(cards: CardData.faceCodes, size: size)
})
}
}
}
In addition, it also simplifies the RowView:
var body: some View {
HStack(spacing: 0) {
ForEach(cards, id: \.self) { card in
HStack(spacing: 0) {
CardView(faceCode: card)
.frame(width: cardWidth, height: height)
}
.frame(width: cardSpacing, alignment: .leading)
}
}
}
Further improvements can be had by storing rows/columns of CardViews inside CardData rather than the card title strings. This will eliminate the need to recreate a full set of (in my case, complex) CardViews in the View code.
The final end result now looks like this:
Is there any way to use other objc function's variable? Here is the scenario: When I long-press on some point of the view, then menu items come up, and when I press Add button, then I want to create a circle button on that position
//Menu Items
#objc func longPressForView(sender: UILongPressGestureRecognizer) -> CGPoint {
if sender.state == .began {
let menu = UIMenuController.shared
becomeFirstResponder()
let menuItemAdd = UIMenuItem(title: "Add", action: #selector(addCircleMenuItemAction))
let menuItemDelete = UIMenuItem(title: "Delete", action: #selector(handleMenuItemAction))
menu.menuItems = [menuItemAdd, menuItemDelete]
let location = sender.location(in: sender.view)
let menuLocation = CGRect(x: location.x, y: location.y, width: 0, height: 0)
menu.showMenu(from: sender.view!, rect: menuLocation)
//I tried to return this location but I don't know how to use it
return location
}
}
//Click Menu Items' Add Button to add a circle on that position.
#objc func addCircleMenuItemAction() {
let longPressedlocation = longPressForView(sender: )
print("Add item tapped")
view.addSubview(addCircle)
addCircle.layer.cornerRadius = CGFloat(circleAddDefaultSize/2)
addCircle.translatesAutoresizingMaskIntoConstraints = false
addCircle.backgroundColor = .black
//Here is where I want to use the location to add a circle
addCircle.frame = CGRect(origin: longPressedlocation?????, size: CGSize(width: circleAddDefaultSize,height: circleAddDefaultSize))
You can create a global variable and use that inside both functions, something like this
//Menu Items
var location: CGPoint
#objc func longPressForView(sender: UILongPressGestureRecognizer) -> CGPoint {
if sender.state == .began {
let menu = UIMenuController.shared
becomeFirstResponder()
let menuItemAdd = UIMenuItem(title: "Add", action: #selector(addCircleMenuItemAction))
let menuItemDelete = UIMenuItem(title: "Delete", action: #selector(handleMenuItemAction))
menu.menuItems = [menuItemAdd, menuItemDelete]
let location = sender.location(in: sender.view)
let menuLocation = CGRect(x: location.x, y: location.y, width: 0, height: 0)
menu.showMenu(from: sender.view!, rect: menuLocation)
//I tried to return this location but I don't know how to use it
self.location = location
return location
}
}
#objc func addCircleMenuItemAction() {
print("Add item tapped")
view.addSubview(addCircle)
addCircle.layer.cornerRadius = CGFloat(circleAddDefaultSize/2)
addCircle.translatesAutoresizingMaskIntoConstraints = false
addCircle.backgroundColor = .black
//Here is where I want to use the location to add a circle
addCircle.frame = CGRect(origin: self.location, size: CGSize(width: circleAddDefaultSize,height: circleAddDefaultSize))
}
the problem with your code is, when you long-press the view, the longPressForView gets executed and when you hit the button, it gets executed again inside the function: let longPressedlocation = longPressForView(sender: ), in this line. so the output won't be the same.
Problem
How can I modify the scroll target of a scrollView? I am looking for kind of a replacement for the "classic" scrollView delegate method
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
...where we can modfify the targeted scrollView.contentOffset via targetContentOffset.pointee for instance to create a custom paging behaviour.
Or in other words: I do want to create a paging effect in a (horizontal) scrollView.
What I have tried ie. is something like this:
ScrollView(.horizontal, showsIndicators: true, content: {
HStack(alignment: VerticalAlignment.top, spacing: 0, content: {
card(title: "1")
card(title: "2")
card(title: "3")
card(title: "4")
})
})
// 3.
.content.offset(x: self.dragState.isDragging == true ? self.originalOffset : self.modifiedOffset, y: 0)
// 4.
.animation(self.dragState.isDragging == true ? nil : Animation.spring())
// 5.
.gesture(horizontalDragGest)
Attempt
This is what I tried (besides a custom scrollView approach):
A scrollView has a content area larger then screen space to enable scrolling at all.
I created a DragGesture() to detect if there is a drag going on. In the .onChanged and .onEnded closures I modified my #State values to create a desired scrollTarget.
Conditionally fed in both the original unchanged and the new modified values into the .content.offset(x: y:) modifier - depending on the dragState as a replacement for missing scrollDelegate methods.
Added animation acting conditionally only when drag has ended.
Attached the gesture to the scrollView.
Long story short. It doesn't work.
I hope I got across what my problem is.
Any solutions out there? Looking forward to any input. Thanks!
I have managed to achieve a paging behaviour with a #Binding index. The solution might look dirty, I'll explain my workarounds.
The first thing I got wrong, was to get alignment to .leading instead of the default .center, otherwise the offset works unusual. Then I combined the binding and a local offset state. This kinda goes against the "Single source of truth" principle, but otherwise I had no idea how to handle external index changes and modify my offset.
So, my code is the following
struct SwiftUIPagerView<Content: View & Identifiable>: View {
#Binding var index: Int
#State private var offset: CGFloat = 0
#State private var isGestureActive: Bool = false
// 1
var pages: [Content]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
page
.frame(width: geometry.size.width, height: nil)
}
}
}
// 2
.content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
// 3
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture().onChanged({ value in
// 4
self.isGestureActive = true
// 5
self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
}).onEnded({ value in
if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
self.index += 1
}
if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
self.index -= 1
}
// 6
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
// 7
DispatchQueue.main.async { self.isGestureActive = false }
}))
}
}
}
you may just wrap your content, I used it for "Tutorial Views".
this a trick to switch between external and internal state changes
.leading is mandatory if you don't want to translate all offsets to center.
set the state to local state change
calculate the full offset from the gesture delta (*-1) plus the previous index state
at the end set the final index based on the gesture predicted end, while rounding the offset up or down
reset the state to handle external changes to index
I have tested it in the following context
struct WrapperView: View {
#State var index: Int = 0
var body: some View {
VStack {
SwiftUIPagerView(index: $index, pages: (0..<4).map { index in TODOView(extraInfo: "\(index + 1)") })
Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
}
.pickerStyle(SegmentedPickerStyle())
.padding()
}
}
}
where TODOView is my custom view that indicates a view to implement.
I hope I get the question right, if not please specify which part should I focus on. Also I welcome any suggestions to remove the isGestureActive state.
#gujci your solution is perfect, for more general usage, make it accept Models and view builder as in (note the I pass the geometry size in the builder) :
struct SwiftUIPagerView<TModel: Identifiable ,TView: View >: View {
#Binding var index: Int
#State private var offset: CGFloat = 0
#State private var isGestureActive: Bool = false
// 1
var pages: [TModel]
var builder : (CGSize, TModel) -> TView
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
self.builder(geometry.size, page)
}
}
}
// 2
.content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index))
// 3
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture().onChanged({ value in
// 4
self.isGestureActive = true
// 5
self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index)
}).onEnded({ value in
if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 {
self.index += 1
}
if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 {
self.index -= 1
}
// 6
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
// 7
DispatchQueue.main.async { self.isGestureActive = false }
}))
}
}
}
and can be used as :
struct WrapperView: View {
#State var index: Int = 0
#State var items : [(color:Color,name:String)] = [
(.red,"Red"),
(.green,"Green"),
(.yellow,"Yellow"),
(.blue,"Blue")
]
var body: some View {
VStack(spacing: 0) {
SwiftUIPagerView(index: $index, pages: self.items.identify { $0.name }) { size, item in
TODOView(extraInfo: item.model.name)
.frame(width: size.width, height: size.height)
.background(item.model.color)
}
Picker(selection: self.$index.animation(.easeInOut), label: Text("")) {
ForEach(0..<4) { page in Text("\(page + 1)").tag(page) }
}
.pickerStyle(SegmentedPickerStyle())
}.edgesIgnoringSafeArea(.all)
}
}
with the help of some utilities :
struct MakeIdentifiable<TModel,TID:Hashable> : Identifiable {
var id : TID {
return idetifier(model)
}
let model : TModel
let idetifier : (TModel) -> TID
}
extension Array {
func identify<TID: Hashable>(by: #escaping (Element)->TID) -> [MakeIdentifiable<Element, TID>]
{
return self.map { MakeIdentifiable.init(model: $0, idetifier: by) }
}
}
#gujci, thank you for interesting example. I've played with it and removed the isGestureActive state. Full example may be found in my gist.
struct SwiftUIPagerView<Content: View & Identifiable>: View {
#State private var index: Int = 0
#State private var offset: CGFloat = 0
var pages: [Content]
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
page
.frame(width: geometry.size.width, height: nil)
}
}
}
.content.offset(x: self.offset)
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture()
.onChanged({ value in
self.offset = value.translation.width - geometry.size.width * CGFloat(self.index)
})
.onEnded({ value in
if abs(value.predictedEndTranslation.width) >= geometry.size.width / 2 {
var nextIndex: Int = (value.predictedEndTranslation.width < 0) ? 1 : -1
nextIndex += self.index
self.index = nextIndex.keepIndexInRange(min: 0, max: self.pages.endIndex - 1)
}
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
})
)
}
}
}
As far as I know scrolls in swiftUI doesn't support anything potentially useful such as scrollViewDidScroll or scrollViewWillEndDragging yet. I suggest using either classic UIKit views for making very custom behavior and cool SwiftUI views for anything that is easier. I've tried that a lot and it actually works! Have a look at this guide. Hope that helps
Alternative solution would be to integrate UIKit into SwiftUI using UIViewRepresentative which links UIKit components with SwiftUI. For additional leads and resources, see how Apple suggests you interface with UIKit: Interfacing with UIKit. They have a good example that shows to page between images and track selection index.
Edit: Until they (Apple) implement some sort of content offset that effects the scroll instead of the entire view, this is their suggested solution since they knew the initial release of SwiftUI wouldn't encompass all functionality of UIKit.
Details
Xcode 14
Swift 5.6.1
Requirements
I do not want to use integration with UIKit (clean SwiftUI ONLY)
I do not want to scroll to the any "ID", I want to scroll to the point
Solution
import SwiftUI
#available(iOS 14.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
struct ExtendedScrollView<Content>: View where Content: View {
private let contentProvider: _AligningContentProvider<Content>
// Main Idea from: https://github.com/edudnyk/SolidScroll/blob/main/Sources/SolidScroll/ScrollView.swift
private var config: _ScrollViewConfig
init(config: _ScrollViewConfig = _ScrollViewConfig(),
#ViewBuilder content: () -> Content) {
contentProvider = _AligningContentProvider(content: content(), horizontal: .center, vertical: .center)
self.config = config
}
init(_ axes: Axis.Set = .vertical,
showsIndicators: Bool = true,
#ViewBuilder content: () -> Content) {
var config = _ScrollViewConfig()
config.showsHorizontalIndicator = axes.contains(.horizontal) && showsIndicators
config.showsVerticalIndicator = axes.contains(.vertical) && showsIndicators
self.init(config: config, content: content)
}
init(config: () -> _ScrollViewConfig,
#ViewBuilder content: () -> Content) {
self.init(config: config(), content: content)
}
var body: some View {
_ScrollView(contentProvider: contentProvider, config: config)
}
}
extension _ContainedScrollViewKey: PreferenceKey {}
// MARK: Track ScrollView Scrolling
struct TrackableExtendedScrollView: ViewModifier {
let onChange: (_ScrollViewProxy?) -> Void
func body(content: Content) -> some View {
content
.onPreferenceChange(_ContainedScrollViewKey.self, perform: onChange)
}
}
extension View {
func onScrollChange(perform: #escaping (_ScrollViewProxy?) -> Void) -> some View {
modifier(TrackableExtendedScrollView(onChange: perform))
}
}
Usage Sample
private var gridItemLayout = (0..<40).map { _ in
GridItem(.fixed(50), spacing: 0, alignment: .leading)
}
// ....
ExtendedScrollView() {
LazyHGrid(rows: gridItemLayout) {
ForEach((0..<numberOfRows*numberOfColumns), id: \.self) { index in
let color = (index/numberOfRows)%2 == 0 ? Color(0x94D2BD) : Color(0xE9D8A6)
Text("\(index)")
.frame(width: 50)
.frame(maxHeight: .infinity)
}
}
}
.onScrollChange { proxy in
// let offset = proxy?.contentOffset.y
}
Full Sample
Implementation details
First column and first row are always on the screen
There are 3 "CollectionView":
first row "CollectionView"
first column "CollectionView"
main content "CollectionView"
All "CollectionView" are synced (if you scroll one "CollectionView", another will also be scrolled)
Do not forget to paste The Solution code here
import SwiftUI
import Combine
struct ContentView: View {
private let columWidth: CGFloat = 50
private var gridItemLayout0 = [GridItem(.fixed(50), spacing: 0, alignment: .leading)]
private var gridItemLayout1 = [GridItem(.fixed(50), spacing: 0, alignment: .leading)]
private var gridItemLayout = (0..<40).map { _ in
GridItem(.fixed(50), spacing: 0, alignment: .leading)
}
#State var text: String = "scrolling not detected"
#State private var scrollViewProxy1: _ScrollViewProxy?
#State private var tableContentScrollViewProxy: _ScrollViewProxy?
#State private var tableHeaderScrollViewProxy: _ScrollViewProxy?
private let numberOfColumns = 50
private let numberOfRows = 40
let headerColor = Color(0xEE9B00)
let firstColumnColor = Color(0x0A9396)
let headerTextColor = Color(.white)
let horizontalSpacing: CGFloat = 6
let verticalSpacing: CGFloat = 0
let firstColumnWidth: CGFloat = 100
let columnWidth: CGFloat = 60
var body: some View {
VStack(spacing: 0) {
Text("First column and row are sticked to the content")
.foregroundColor(.gray)
Text(text)
HStack {
Rectangle()
.frame(width: firstColumnWidth-2)
.foregroundColor(.clear)
buildFirstCollectionViewRow()
}
.frame(height: 50)
HStack(alignment: .firstTextBaseline, spacing: horizontalSpacing) {
buildFirstCollectionViewColumn()
buildCollectionViewContent()
}
}
}
#ViewBuilder
private func buildFirstCollectionViewRow() -> some View {
ExtendedScrollView() {
LazyHGrid(rows: gridItemLayout1, spacing: horizontalSpacing) {
ForEach((0..<numberOfColumns), id: \.self) {
let color = $0%2 == 0 ? Color(0x005F73) : Color(0xCA6702)
Text("Value\($0)")
.frame(width: columnWidth)
.frame(maxHeight: .infinity)
.foregroundColor(headerTextColor)
.background(color)
.font(.system(size: 16, weight: .semibold))
}
}
}
.onScrollChange { proxy in
if tableHeaderScrollViewProxy != proxy { tableHeaderScrollViewProxy = proxy }
guard proxy?.isScrolling ?? false else { return }
if tableHeaderScrollViewProxy?.contentOffset.x != tableContentScrollViewProxy?.contentOffset.x,
let offset = proxy?.contentOffset.x {
tableContentScrollViewProxy?.contentOffset.x = offset
}
text = "scrolling: header"
}
}
}
// MARK: Collection View Elements
extension ContentView {
#ViewBuilder
private func buildFirstCollectionViewColumn() -> some View {
ExtendedScrollView() {
LazyHGrid(rows: gridItemLayout, spacing: horizontalSpacing) {
ForEach((0..<numberOfRows), id: \.self) {
Text("multi line text \($0)")
.foregroundColor(.white)
.lineLimit(2)
.frame(width: firstColumnWidth)
.font(.system(size: 16, weight: .semibold))
.frame(maxHeight: .infinity)
.background(firstColumnColor)
.border(.white)
}
}
}
.frame(width: firstColumnWidth)
.onScrollChange { proxy in
if scrollViewProxy1 != proxy { scrollViewProxy1 = proxy }
guard proxy?.isScrolling ?? false else { return }
if scrollViewProxy1?.contentOffset.y != tableContentScrollViewProxy?.contentOffset.y,
let offset = proxy?.contentOffset.y {
tableContentScrollViewProxy?.contentOffset.y = offset
}
text = "scrolling: 1st column"
}
}
#ViewBuilder
private func buildCollectionViewContent() -> some View {
ExtendedScrollView() {
LazyHGrid(rows: gridItemLayout, spacing: horizontalSpacing) {
ForEach((0..<numberOfRows*numberOfColumns), id: \.self) { index in
let color = (index/numberOfRows)%2 == 0 ? Color(0x94D2BD) : Color(0xE9D8A6)
Text("\(index)")
.frame(width: columnWidth)
.frame(maxHeight: .infinity)
.background(color)
.border(.white)
}
}
}
.onScrollChange { proxy in
if tableContentScrollViewProxy != proxy { tableContentScrollViewProxy = proxy }
guard proxy?.isScrolling ?? false else { return }
if scrollViewProxy1?.contentOffset.y != tableContentScrollViewProxy?.contentOffset.y,
let offset = proxy?.contentOffset.y {
self.scrollViewProxy1?.contentOffset.y = offset
}
if tableHeaderScrollViewProxy?.contentOffset.x != tableContentScrollViewProxy?.contentOffset.x,
let offset = proxy?.contentOffset.x {
self.tableHeaderScrollViewProxy?.contentOffset.x = offset
}
text = "scrolling: content"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension Color {
init(_ hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xFF) / 255,
green: Double((hex >> 8) & 0xFF) / 255,
blue: Double(hex & 0xFF) / 255,
opacity: alpha
)
}
}
Full Sample Demo
I have created a Swift framework that creating a button programmatically, and the action of the button is showing a webView and close it, my framework works fine in Swift project but in Objective C it just create button but it is not taking action and does not show my webView,
Note: I am getting the button configuration from server via JSON using SwiftyJSON
below is the my Swift Class in addition to SwiftyJSON in the framework project
import Foundation
import UIKit
import WebKit
#objc public class predictionButton : NSObject {
public override init(){
}
var buttonConfigApi = "XXXXXXXXX"
var buttonTextColor = String()
var buttonText = String()
var buttonBackgroundColor = String()
var buttonVisibility = Bool()
var indexURL = String()
var authURL = String()
var module = String()
var buttonAlignment = String()
var globalURL = String()
var webViewX = CGFloat()
var webViewY = CGFloat()
var webViewXiWidth = CGFloat()
var webViewHeight = CGFloat()
#objc public var globalView = UIView()
var parseFlag = Bool()
var buttonAlignmentValue = String()
#objc public func addButtonPredict(view: UIView, phone: String, token: String){
parseJSON()
sleep(5)
var phonTok = "{'a':\(phone),'b':\(token)}"
let utf8str = phonTok.data(using: String.Encoding.utf8)
let data = (phonTok).data(using: String.Encoding.utf8)
let base64 = data!.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
var finalURL = String()
self.globalView = view
finalURL = "\(self.indexURL)?d=\(base64)"
self.globalURL = finalURL
let button = UIButton(frame: buttonPosition(view: view, alignValue: buttonAlignment))
print((self.globalURL))
button.backgroundColor = hexStringToUIColor(hex: self.buttonBackgroundColor)
print("global color value \(buttonBackgroundColor)")
button.setTitle("\(self.buttonText)", for: .normal)
button.setTitleColor(hexStringToUIColor(hex: self.buttonTextColor), for: .normal)
let buttonFontSize = 15
let buttonTitleSize = (buttonText as NSString).size(withAttributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: CGFloat(buttonFontSize + 1))])
button.frame.size.height = buttonTitleSize.height * 2
button.frame.size.width = buttonTitleSize.width * 1.5
view.addSubview(button)
button.tag = 5
button.addTarget(self, action: #selector(nest), for: .touchUpInside)
}
#objc func nest (){
let frame = webViewposition(view: self.globalView)
let webView = WKWebView(frame: frame)
webView.load(NSURLRequest(url: NSURL(string: globalURL)! as URL) as URLRequest)
webView.contentMode = .scaleAspectFit
webView.tag = 100
if let viewWithTag = self.globalView.viewWithTag(100) {
viewWithTag.removeFromSuperview()
}else{
print("No!")
self.globalView.addSubview(webView)
}
}
#objc public func parseJSON(){
print("flag")
let requestURL: NSURL = NSURL(string: buttonConfigApi)!
let urlRequest: URLRequest = URLRequest(url: requestURL as URL)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest as URLRequest) {
(data, response, error) -> Void in
if data != nil {
do
{
let readableJSON = try JSON(data: data!, options: JSONSerialization.ReadingOptions.mutableContainers)
let Name = readableJSON["data"]["buttonText"].stringValue as String!
let buttonTextColor = readableJSON["data"]["buttonTextColorCode"].stringValue as String!
let buttonBackgroundColorCode = readableJSON["data"]["buttonBackgroundColorCode"].stringValue as String!
let visibility = readableJSON["data"]["visibility"].boolValue
let indexURL = readableJSON["data"]["indexURL"].stringValue as String!
let authURL = readableJSON["data"]["authURL"].stringValue as String!
let module = readableJSON["data"]["module"].stringValue as String!
let alignment = readableJSON["data"]["alignment"].stringValue as String!
self.buttonText = Name!
self.buttonTextColor = buttonTextColor!
self.buttonBackgroundColor = buttonBackgroundColorCode!
self.buttonAlignment = self.alignText(text: alignment!)
self.indexURL = indexURL!
self.authURL = authURL!
self.module = module!
print("\(Name!) \n \(buttonTextColor!) \n \(buttonBackgroundColorCode!) \n \(indexURL!) \n \(alignment!)")
print("done")
print("Color value:\(buttonBackgroundColorCode!)")
} catch {
print(error)
}
}
}
task.resume()
}
#objc func alignText (text: String) -> String {
var alignText = String()
let newString = text.replacingOccurrences(of: "_", with: "",
options: .literal, range: nil)
return newString.lowercased()
}
#objc func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
print("became gray")
return UIColor.gray
}
var rgbValue:UInt32 = 0
Scanner(string: cString).scanHexInt32(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
#objc func webViewposition(view: UIView) -> CGRect {
var frame = CGRect()
if UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
frame = CGRect(x: (view.frame.width) - (view.frame.width / 4) - 120 , y: 20 , width: view.frame.width / 3 , height: view.frame.height - 60 )
} else if UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
frame = CGRect(x: (view.frame.width) - (view.frame.width / 4) - 120, y: 20 , width: view.frame.width / 3 , height: view.frame.height - 60 )
} else if UIDevice.current.orientation == UIDeviceOrientation.portrait {
frame = CGRect(x: 0, y: 110, width: view.frame.width, height: view.frame.height - 60 )
} else if UIDevice.current.orientation == UIDeviceOrientation.portraitUpsideDown {
frame = CGRect(x: 0, y: 100, width: view.frame.width, height: view.frame.height - 60 )
}
return frame
}
#objc func buttonPosition(view: UIView, alignValue: String)-> CGRect {
var position = CGRect()
switch (alignValue){
case "lefttop":
print("button top left")
position = CGRect(x: 25, y: 50, width: 100, height: 100)
case "righttop":
print("button top right")
position = CGRect(x: view.frame.width - 150, y: 50, width: 100, height: 100)
case "letbottom":
print("button bottom left")
position = CGRect(x: 25, y: view.frame.height - 150, width: 100, height: 100)
case "rightbottom":
print("button bottom right")
position = CGRect(x: view.frame.width - 25, y: view.frame.height - 150, width: 100, height: 100)
default:
position = CGRect(x: 25, y: 50, width: 100, height: 100)
}
return position
}
}
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
assert(red >= 0 && red <= 255, "Invalid red component")
assert(green >= 0 && green <= 255, "Invalid green component")
assert(blue >= 0 && blue <= 255, "Invalid blue component")
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) /
255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
}
convenience init(rgb: Int) {
self.init(
red: (rgb >> 16) & 0xFF,
green: (rgb >> 8) & 0xFF,
blue: rgb & 0xFF
)
}
}
extension String {
var numberValue:NSNumber? {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter.number(from: self)
}
}
and I have created a Objective-C and add the built framework in it and its .m file code is below :
#import "ViewController.h"
#import myFrameWork;
#import <myFrameWork/myFrameWork-Swift.h>
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
predictionButton *btn = [[predictionButton alloc] init];
[btn addButtonPredictWithView:self.view phone:#"XXXXXXX"
token:#"XXXXXXXXXXXXXXXXXXXXXXXXXXX"];
}
#end
Note: this framework works fine in Swift project
Here is working swift code:
import UIKit
import myFrameWork
import WebKit
class ViewController: UIViewController {
var button = predictionButton()
override func viewDidLoad() {
super.viewDidLoad()
button.addButtonPredict(view: self.view, phone: "XXXXXXXXXX", token: "XXXXXXXXXXXXXXXXXXXXXXXXX")
}
I created a UICollectionView named UserProfileController.
I created a UICollectionViewCell named UserProfilePhotoCell.
I created avplayer in UserProfilePhotoCell.
When you click on Avplayer the video starts.
When I click on the 1st AVPlayer on the cell,
The first, fifth, ninth cells play AVplayer.
How can I fix?
Issue Video = https://ibb.co/bMwOAe
import UIKit
import Alamofire
import AVKit
import AVFoundation
class UserProfileController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let screen = ScreenProp()
var posts = [ProfileVideo]()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Profil Detay"
collectionView?.backgroundColor = .white
collectionView?.register(UserProfileHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "headerID")
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.register(UserProfilePhotoCell.self, forCellWithReuseIdentifier: "cellId")
veriCek()
}
var user: User?
func veriCek(){
//http post başlangıç
// LoadingOverlay.shared.showOverlay(view: UIApplication.shared.keyWindow!)
//
// guard let confirm = phoneInput.text else {return}
let defaults = UserDefaults.standard
let userID = defaults.integer(forKey: "userID")
let acToken = defaults.string(forKey: "acToken")
let parameters: Parameters = [
"userID": userID,
"acToken": acToken ?? ""
]
Alamofire.request("********************************", method: .post, parameters: parameters).validate().responseJSON { response in
switch response.result {
case .success:
print(response.data!)
if let result = response.result.value {
let json = result as! NSDictionary
print(json)
let dataArray = json["sonuclar"] as! NSArray;
print("Data items count: \(dataArray.count)")
for item in dataArray { // loop through data items
let obj = item as! NSDictionary
// let profileVideo = ProfileVideo(dictionary: obj)
// print(profileVideo.artistName)
let artistName = obj.value(forKey: "artistName") as! String
let commentlikes = obj.value(forKey: "commentlikes") as! String
let likeStatus = obj.value(forKey: "likeStatus") as! String
let name = obj.value(forKey: "name") as! String
let profilePics = obj.value(forKey: "profilePics") as! String
let time = obj.value(forKey: "time") as! String
let userID = obj.value(forKey: "userID") as! String
let username = obj.value(forKey: "username") as! String
let videoContent = obj.value(forKey: "videoContent") as! String
let videoID = obj.value(forKey: "videoID") as! String
let videoLikes = obj.value(forKey: "videoLikes") as! String
let videoName = obj.value(forKey: "videoName") as! String
let videoPath = obj.value(forKey: "videoPath") as! String
let view = obj.value(forKey: "view") as! String
let post = ProfileVideo(artistName: artistName, commentlikes: commentlikes, likeStatus: likeStatus, name: name, profilePics: profilePics, time: time, userID: userID, username: username, videoContent: videoContent, videoID: videoID, videoLikes: videoLikes, videoName: videoName, videoPath: videoPath, view: view, status: 0)
self.posts.append(post)
}
self.user = User(dictionary: json as! [String : Any])
self.navigationItem.title = self.user?.username
self.collectionView?.reloadData()
}
case .failure(let error):
print(error)
}
}
// http post bitiş
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "headerID", for: indexPath) as! UserProfileHeader
header.user = self.user
return header
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! UserProfilePhotoCell
var dizi = posts[indexPath.item]
print("\(indexPath.item) = \(dizi.status)")
cell.post = dizi
return cell
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize{
return CGSize(width: view.frame.width ,height: CGFloat(screen.heightHesap(x: 246)))
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = view.frame.width
return CGSize(width: width, height: 500)
}
}
struct User {
let name: String
let username: String
let profileImageUrl: String
let follow: String
let follower: String
let videoCount: String
init(dictionary: [String: Any]) {
self.username = dictionary["username"] as! String
self.profileImageUrl = dictionary["profilePics"] as! String
self.name = dictionary["name"] as! String
self.follow = dictionary["follow"] as! String
self.follower = dictionary["follower"] as! String
self.videoCount = dictionary["videoCount"] as! String
}
}
struct ProfileVideo {
let artistName: String
let commentlikes: String
var likeStatus: String
let name: String
let profilePics: String
let time: String
let userID: String
let username: String
let videoContent: String
let videoID: String
let videoLikes: String
let videoName: String
let videoPath: String
let view: String
var status: Int
}
UserProfilePhotoCell
import UIKit
import AVKit
import AVFoundation
import Alamofire
class UserProfilePhotoCell: UICollectionViewCell {
var screen = ScreenProp()
var playerLayer = AVPlayerLayer()
var player = AVPlayer()
var moviePlayerController = AVPlayerViewController()
let defaults = UserDefaults.standard
var status = 0
let videoHeader: UIImageView = {
let imageView = UIImageView(image: #imageLiteral(resourceName: "videoHeader1"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .red
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let videoFooter: UIImageView = {
let imageView = UIImageView(image:#imageLiteral(resourceName: "videoFooter"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .none
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let clapsDeactive: UIImageView = {
let imageView = UIImageView(image: #imageLiteral(resourceName: "applause"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .none
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let clapsActive: UIImageView = {
let imageView = UIImageView(image: #imageLiteral(resourceName: "applause_on"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .none
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let comment: UIImageView = {
let imageView = UIImageView(image: #imageLiteral(resourceName: "comments"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .none
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let videoName: UILabel = {
let textView = UILabel()
textView.text = "Video Adı";
textView.textColor = CustomColor.white;
textView.font = CustomFont.textStyle16;
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let userName: UILabel = {
let textView = UILabel()
textView.text = "username";
textView.textColor = CustomColor.bblack;
textView.font = CustomFont.textStyle16;
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let location: UILabel = {
let textView = UILabel()
textView.text = "location";
textView.textColor = CustomColor.battleshipGreyTwo;
textView.font = CustomFont.textStyle16;
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let profilePhoto: UIImageView = {
var screen1 = ScreenProp()
let imageView = UIImageView(image: #imageLiteral(resourceName: "profile_photo"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
imageView.contentMode = .scaleToFill
imageView.layer.cornerRadius = CGFloat(screen1.heightHesap(x: 37)/2)
imageView.clipsToBounds = true
return imageView
}()
let videoCover: UIImageView = {
var screen1 = ScreenProp()
let imageView = UIImageView(image: #imageLiteral(resourceName: "video-sample"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .white
imageView.contentMode = .scaleToFill
imageView.clipsToBounds = true
return imageView
}()
let clapsText: UILabel = {
let textView = UILabel()
textView.text = "0";
textView.textColor = CustomColor.bblack;
textView.font = CustomFont.textStyle3;
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let commentsText: UILabel = {
let textView = UILabel()
textView.text = "0";
textView.textColor = CustomColor.bblack;
textView.font = CustomFont.textStyle3;
textView.textAlignment = .center
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
let content: UITextView = {
let textView = UITextView()
textView.text = "Donec pretium est sit amet ipsum fringilla feugiat. Aliquam erat volutpat. Maecenas scelerisque,";
textView.textColor = CustomColor.cloudyBlue;
textView.font = CustomFont.textStyle3;
textView.textAlignment = .left
textView.isEditable = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isScrollEnabled = false
return textView
}()
var set = 0
var ratePlayer = 0
var post: ProfileVideo? {
didSet{
print(post?.status)
if(set == 0){
// kur(str: (post?.videoPath)!)
}
videoName.text = "\(post?.artistName ?? "") - \(post?.videoName ?? "")"
userName.text = post?.username
clapsText.text = post?.videoLikes as? String
commentsText.text = post?.commentlikes
content.text = post?.videoContent
var url = URL(string: post?.profilePics ?? "")
ImageLoader.image(for: url!) { image in
self.profilePhoto.image = image
}
if(post?.likeStatus == "0"){
status = 1
self.clapsDeactive.image = UIImage(named: "applause")
}else{
status = 0
self.clapsDeactive.image = UIImage(named: "applause_on")
}
set = set + 1
}
}
func kur(str: String){
print("asdjvhsagdvjsakd")
let videoURL = URL(string: str)
// var myUrl = post.image // post.image is image url
let fileUrl = videoURL
// aPlayer = AVPlayer(URL: fileUrl as! URL)
player = AVPlayer(url: fileUrl!)
moviePlayerController.player = player
moviePlayerController.view.frame = CGRect(x:0, y:37, width:frame.size.width, height:300)
moviePlayerController.videoGravity = AVLayerVideoGravity.resizeAspectFill.rawValue
moviePlayerController.view.sizeToFit()
moviePlayerController.showsPlaybackControls = true
addSubview(moviePlayerController.view)
//moviePlayerController.player?.play()
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(videoHeader)
addSubview(videoName)
addSubview(profilePhoto)
addSubview(userName)
addSubview(location)
addSubview(videoFooter)
addSubview(clapsDeactive)
addSubview(comment)
addSubview(clapsText)
addSubview(commentsText)
addSubview(content)
addSubview(videoCover)
setup()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(like(tapGestureRecognizer:)))
clapsDeactive.isUserInteractionEnabled = true
clapsDeactive.addGestureRecognizer(tapGestureRecognizer)
let tapGestureRecognizer2 = UITapGestureRecognizer(target: self, action: #selector(playVideo(tapGestureRecognizer:)))
videoCover.isUserInteractionEnabled = true
videoCover.addGestureRecognizer(tapGestureRecognizer2)
}
func setup(){
videoHeader.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: frame.size.width, height: CGFloat(screen.heightHesap(x: 37)))
videoName.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 9)), paddingLeft: CGFloat(screen.widthHesap(x: 40)), paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
profilePhoto.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 360)), paddingLeft: CGFloat(screen.widthHesap(x: 20)), paddingBottom: 0, paddingRight: 0, width: CGFloat(screen.widthHesap(x: 37)), height: CGFloat(screen.heightHesap(x: 37)))
userName.anchor(top: profilePhoto.topAnchor, left: profilePhoto.rightAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: CGFloat(screen.widthHesap(x: 11)), paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
location.anchor(top: userName.bottomAnchor, left: profilePhoto.rightAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: CGFloat(screen.widthHesap(x: 11)), paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
videoFooter.anchor(top: topAnchor, left: nil, bottom: nil, right: rightAnchor, paddingTop: CGFloat(screen.heightHesap(x: 356)), paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: CGFloat(screen.widthHesap(x: 180)), height: CGFloat(screen.heightHesap(x: 50)))
clapsDeactive.anchor(top: videoFooter.topAnchor, left: videoFooter.leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 9)), paddingLeft: CGFloat(screen.widthHesap(x: 29)), paddingBottom: 0, paddingRight: 0, width: CGFloat(screen.widthHesap(x: 33)), height: CGFloat(screen.heightHesap(x: 28)))
comment.anchor(top: videoFooter.topAnchor, left: videoFooter.leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 15)), paddingLeft: CGFloat(screen.widthHesap(x: 110)), paddingBottom: 0, paddingRight: 0, width: CGFloat(screen.widthHesap(x: 23)), height: CGFloat(screen.heightHesap(x: 23)))
clapsText.anchor(top: videoFooter.topAnchor, left: videoFooter.leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 17)), paddingLeft: CGFloat(screen.widthHesap(x: 67)), paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
commentsText.anchor(top: videoFooter.topAnchor, left: videoFooter.leftAnchor, bottom: nil, right: nil, paddingTop: CGFloat(screen.heightHesap(x: 17)), paddingLeft: CGFloat(screen.widthHesap(x: 142)), paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
var hesap = frame.size.width - CGFloat(screen.widthHesap(x: 40))
content.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: rightAnchor, paddingTop: CGFloat(screen.heightHesap(x: 420)), paddingLeft: CGFloat(screen.widthHesap(x: 20)), paddingBottom: 0, paddingRight: CGFloat(screen.widthHesap(x: 20)), width: hesap, height: CGFloat(screen.heightHesap(x: 50)))
videoCover.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 37, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: frame.size.width, height: 300)
}
#objc func playVideo(tapGestureRecognizer: UITapGestureRecognizer)
{
kur(str: (post?.videoPath)!)
}
#objc func like(tapGestureRecognizer: UITapGestureRecognizer)
{
let userID = defaults.string(forKey: "userID")
let acToken = defaults.string(forKey: "acToken")
let parameters: Parameters = [
"userID": userID ?? "",
"acToken": acToken ?? "",
"videoID": post?.videoID ?? "",
"status": status,
"videoUserID": post?.userID ?? ""
]
Alamofire.request("***********************************", method: .post, parameters: parameters).validate().responseJSON { response in
switch response.result {
case .success:
print(response.data!)
if let result = response.result.value {
let json = result as! NSDictionary
let statusCode = json["statusCode"]
let status = json["status"]
var like = json["likeCount"] as? String
if(statusCode as? Int == 10){
print("Beğendim.")
self.post?.likeStatus = "1"
print(self.post?.likeStatus)
self.clapsDeactive.image = UIImage(named: "applause_on")
self.clapsText.text = like
}else{
print("Beğenemedim.")
self.post?.likeStatus = "0"
self.clapsDeactive.image = UIImage(named: "applause")
self.clapsText.text = like
}
}
case .failure(let error):
print(error)
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Just use this code for play video in the cell with expect index. It will help you a lot and for other also ,
class UserProfilePhotoCell: UICollectionViewCell {
#IBOutlet var viewForVideo: UIView!
#IBOutlet var btnPlayVideo: UIButton!
var blockVideoPlay : ((_ sender : UIButton)->())?
override func awakeFromNib() {
super.awakeFromNib()
removeSubLayerFromVideo()
self.setupMoviePlayer()
}
#IBAction func btnPlayVideoCLK(_ sender: UIButton) {
if blockVideoPlay != nil {
blockVideoPlay!(sender)
}
}
func removeSubLayerFromVideo() {
if let layerArray = viewForVideo.layer.sublayers, layerArray.isEmpty {
for layer in layerArray {
if layer.name == "\(viewForVideo.tag)" {
layer.removeFromSuperlayer()
}
}
}
}
func setupMoviePlayer(){
self.avPlayer = AVPlayer.init(playerItem: self.videoPlayerItem)
avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
avPlayerLayer?.name = "\(viewForVideo.tag)"
avPlayer?.volume = 3
avPlayer?.actionAtItemEnd = .none
avPlayerLayer?.frame = CGRect(x: 0, y: 0, width: UIScreen.width, height: UIScreen.height)
self.viewForVideo.backgroundColor = .clear
self.viewForVideo.layer.insertSublayer(avPlayerLayer!, at: 0)
// This notification is fired when the video ends, you can handle it in the method.
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: avPlayer?.currentItem)
}
func stopPlayback(){
if let player = self.avPlayer {
if player.isPlaying {
player.pause()
}
}
}
func startPlayback(){
self.avPlayer?.play()
}
// A notification is fired and seeker is sent to the beginning to loop the video again
#objc func playerItemDidReachEnd(notification: Notification) {
//let p: AVPlayerItem = notification.object as! AVPlayerItem
//p.seek(to: kCMTimeZero)
btnPlayVideo.isSelected = false
self.avPlayer?.currentItem?.seek(to: kCMTimeZero, completionHandler: { (success) in
if success {
self.avPlayer?.pause()
}
})
}
}
For View Controller use this code also you can post via var all data in the cell and from the cell, you can set URL path it works fine make sure when view Will Disappear you have to stop the player
class UserProfileController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
for cell in mediaListCollection.visibleCells {
if let cell = cell as? UserProfilePhotoCell {
cell.stopPlayback()
}
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! UserProfilePhotoCell
let url = URL(fileURLWithPath: path, isDirectory: true)
cell.videoPlayerItem = AVPlayerItem(url: url)
cell.viewForVideo.tag = indexPath.row+1
cell.blockVideoPlay = { (sender) -> Void in
sender.isSelected = !sender.isSelected
if sender.isSelected {
cell.startPlayback()
}
else {
cell.stopPlayback()
}
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if let cell = cell as? UserProfilePhotoCell {
cell.stopPlayback()
if cell.btnPlayVideo.isSelected {
cell.btnPlayVideo.isSelected = false
}
}
}
}
Also use this extension for check player is currently playing or not
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}