I am struggling with a strange bug with my iOS app. When using a simulator with iOS 13 application works as it should, but when using iOS 14 buttons, switches, and other functionalities are not responding. There is no error output in the console.
I don't understand why this happens only with XCode 11 and iOS 14.
This a snippet with initializing one of my buttons in the View.
let logButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setLogInButton()
button.setTitle(NSLocalizedString("log_in", comment: ""), for: .normal)
return button
}()
Here I'm assigning the target to the button.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initConstraints()
backgroundColor = UIColor().settingsTableViewCellColor()
logButton.addTarget(self, action: #selector(buttonLogInLogOut), for: .touchUpInside)
}
There is the action
#objc func buttonLogInLogOut(_ sender: UIButton){
print("Log in clicked")
delegate?.logInLogOutFunc(sender)
}
As I said the buttons (switches, and others) are not responding ONLY in iOS 14.
It looks like targetActions are not working.
Thanks for any kind of help.
Regards
Matt
I had the same problem with a button in a table cell not working
For some reason you have to add the button to the contentView of the cell instead of the cell itself as follows
cell.contentView.addSubView(button)
Worked for me afterwards
Just follow this solution
For Cell:
class CommentCell: UICollectionViewCell {
private let commentViewCell: CommentViewCell = {
let view = CommentViewCell()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(commentViewCell)
NSLayoutConstraint.activate([
commentViewCell.leadingAnchor.constraint(equalTo: leadingAnchor),
commentViewCell.trailingAnchor.constraint(equalTo: trailingAnchor),
commentViewCell.topAnchor.constraint(equalTo: topAnchor),
commentViewCell.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setData(item: CommentViewModel) {
commentViewCell.setData(item: item)
}
}
For View:
class CommentViewCell: UIView {
private let lblId: CustomClick = {
let view = CustomClick()
view.backgroundColor = .cyan
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let lblMessage: UILabel = {
let view = UILabel()
view.backgroundColor = .cyan
view.textColor = .black
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let lblDate: UILabel = {
let view = UILabel()
view.backgroundColor = .systemIndigo
view.textColor = .black
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .brown
addSubview(lblId)
addSubview(lblMessage)
addSubview(lblDate)
NSLayoutConstraint.activate([
lblId.topAnchor.constraint(equalTo: topAnchor),
lblId.trailingAnchor.constraint(equalTo: trailingAnchor),
lblId.leadingAnchor.constraint(equalTo: leadingAnchor),
lblMessage.trailingAnchor.constraint(equalTo: trailingAnchor),
lblMessage.leadingAnchor.constraint(equalTo: leadingAnchor),
lblMessage.topAnchor.constraint(equalTo: lblId.bottomAnchor),
lblDate.trailingAnchor.constraint(equalTo: trailingAnchor),
lblDate.leadingAnchor.constraint(equalTo: leadingAnchor),
lblDate.topAnchor.constraint(equalTo: lblMessage.bottomAnchor),
])
lblId.addTarget(self, action: #selector(onClick), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func onClick() {
print("this is click button")
}
func setData(item: CommentViewModel) {
lblId.setData(item: item.id)
lblMessage.text = item.meesage
lblDate.text = item.date
lblDate.textColor = item.cellColor
}
class CustomClick: UIControl {
private let lblId: UILabel = {
let view = UILabel()
view.textColor = .black
view.backgroundColor = .systemPink
view.font = .boldSystemFont(ofSize: 16)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(lblId)
NSLayoutConstraint.activate([
lblId.topAnchor.constraint(equalTo: topAnchor),
lblId.trailingAnchor.constraint(equalTo: trailingAnchor),
lblId.leadingAnchor.constraint(equalTo: leadingAnchor),
lblId.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setData(item: String) {
lblId.text = item
}
}
}
Related
I use this code to load my html file with text in WKWebView:
do {
guard let filePath = Bundle.main.path(forResource: "\(readBookNumber)", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: UserDefaults.standard.integer(forKey: "textSize"), fontColor: textColor)
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
}
catch {
print ("File HTML error")
}
and this code to load the page where the user stopped reading last time:
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
I use code for loading last page in this method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
At first I used deadline: .now() + 0.1, but that didn't work. Because the last read page was loaded initially, and after a few seconds I see my text on the first page. I change it to deadline: .now() + 0.5 and the text loads fine from the last page read. Its was 700 pages. But now I want to load another text with 1700 pages. And I have same problem like first time. I can change deadline: .now() + 1.0 and my text will load fine. But I think this is not the best solution. I run it on my iPhone X. But maybe if I run it on iPad mini 2 I should change deadline: .now() + 10.0 because iPad mini 2 not very powerful. How to solve the problem?
Update based on #DPrice code:
If I use this code:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
I have same bad result like in my code.
But if I use this code:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad\(self.readBookNumber)"))
}
}
}
}
Everything works fine. And my last page loading fine. But it does not solve the problem in my question.
Here is a modified version of your ViewController class:
import UIKit
import WebKit
class ViewController: UIViewController, UIScrollViewDelegate, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
#IBOutlet weak var pagesLabel: UILabel!
var readBookNumber = 0
let headerString = "<meta name=\"viewport\" content=\"initial-scale=1.0\" />"
var textSize = 3
var contentSize: CGSize = .zero
override func viewDidLoad() {
super.viewDidLoad()
// Web View Delegate
webView.scrollView.delegate = self
webView.navigationDelegate = self
webView.scrollView.isPagingEnabled = true
webView.scrollView.alwaysBounceVertical = false
webView.scrollView.showsHorizontalScrollIndicator = true
webView.scrollView.showsVerticalScrollIndicator = false
webView.scrollView.panGestureRecognizer.isEnabled = false
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
webView.scrollView.bouncesZoom = false
self.webView.isOpaque = false;
self.webView.backgroundColor = .clear
// Load File
do {
guard let filePath = Bundle.main.path(forResource: "0", ofType: "html")
else {
print ("File reading error")
return
}
var content = try String(contentsOfFile: filePath, encoding: .utf8)
let baseUrl = URL(fileURLWithPath: filePath)
content.changeHtmlStyle(font: "Iowan-Old-Style", fontSize: 4, fontColor: "black")
webView.loadHTMLString(headerString+content, baseURL: baseUrl)
// add content size Observer
webView.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
catch {
print ("File HTML error")
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
let contentSize = webView.scrollView.contentSize
if contentSize != self.contentSize {
self.contentSize = contentSize
DispatchQueue.main.async {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}
}
}
}
// MARK: - webView Scroll View
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.stoppedScrolling()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.stoppedScrolling()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var currentPage = Int((webView.scrollView.contentOffset.x / webView.scrollView.frame.size.width) + 1)
let pageCount = Int(webView.scrollView.contentSize.width / webView.scrollView.frame.size.width)
if currentPage == 0 {
currentPage = 1
} else {
}
if !webView.isHidden {
pagesLabel.text = "\( currentPage ) из \( pageCount )"
} else {
pagesLabel.text = ""
}
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
webView.scrollView.pinchGestureRecognizer?.isEnabled = false
}
func stoppedScrolling() {
let pageToLoad = Int((webView.scrollView.contentOffset.x))
UserDefaults.standard.set(pageToLoad, forKey: "pageToLoad")
}
// MARK: - loading webView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Маленькая задержка, которую мне хотелось бы использовать
/*DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
}*/
// Большая задержка, которую мне приходится использовать
// don't do this here... we'll do the "auto-scroll" inside the change contentSize Observer
//DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// self.webView.scrollView.contentOffset.x = CGFloat(UserDefaults.standard.integer(forKey: "pageToLoad"))
//}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
}
}
extension String {
mutating func changeHtmlStyle(font: String, fontSize: Int, fontColor: String) {
let style = "<font face='\(font)' size='\(fontSize)' color= '\(fontColor)'>%#"
self = String(format: style, self)
}
}
It uses an Observer to watch the contentSize change in the web view's scroll view.
Note that it is called multiple times - with different values - during the load and layout process, but it may do the job for you.
Also note, though, that you'll need to account for changes in the web view size - for example, if the user rotates the device. So... more to do, but this may get you going.
You can add a property observer and watch the estimated progress of the page load:
override func viewDidLoad() {
super.viewDidLoad()
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
....
}
and observe when the page is being loaded:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == "estimatedProgress") {
if webView.estimatedProgress == 1.0 {
print ("page loaded")
}
}
}
You may be able to predict based on the page number how far into the loading process you need to be before you set your offset.
Instead of observing WKWebView.estimatedProgress you should observe UIScrollView.contentSize because you need to scroll to an available position e.g.:
var positionY: CGFloat = 1000
var contentSize = CGSize(width: 0, height: 0)
override func viewDidLoad() {
super.viewDidLoad()
...
webView?.scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if (keyPath == #keyPath(UIScrollView.contentSize)) {
if let contentSize = webView?.scrollView.contentSize, contentSize != self.contentSize {
self.contentSize = contentSize
if contentSize.height > positionY {
webView?.scrollView.setContentOffset(CGPoint(x: 0, y: positionY), animated: true)
}
}
}
}
I create UIScrollView to be integrated inside SwiftUI view. It contains UIHostingController to host SwiftUI view. When I update UIHostingController, UIScrollView does not change its constraints. I can scroll neither to top nor to bottom. When I try to call viewDidLoad() inside updateUIViewController(_:context:), it works like I expect. Here is my sample code,
struct ContentView: View {
#State private var max = 100
var body: some View {
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: UIScreen.main.bounds.width, height: 100)
.background(Color(red: Double.random(in: 0...255) / 255, green: Double.random(in: 0...255) / 255, blue: Double.random(in: 0...255) / 255))
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView: UIScrollView = UIScrollView()
override func viewDidLoad() {
self.view = UIView()
self.addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
scrollView.delegate = self
scrollView.scrollsToTop = true
scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
let vc = ScrollViewController(rootView: self.content())
return vc
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
viewController.viewDidLoad()
}
}
I don't think it is a good way to do. I want to know if there is the best way to update controller. If anyone knows the best solution, share me please. Thanks.
You are correct, we should never call our own viewDidLoad.
Let’s diagnose the issue, using the view debugger. So, for example, here it is (setting max to 8 to keep it manageable):
Note the height of the hosting controller’s view is 800 (because we have 8 subviews, 100 pt each). So far, so good.
Now tap the “add” button and repeat:
We can see that the problem isn’t the scroll view, but rather the hosting view controller’s view. Even though there are now 10 items, it still thinks the hosting view controller’s view’s height is 800.
So, we can call setNeedsUpdateConstraints and that fixes the problem:
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
Thus:
struct ContentView: View {
#State private var max = 8
var body: some View {
GeometryReader { geometry in // don't reference `UIScreen.main.bounds` as that doesn’t work in split screen multitasking
VStack {
Button("Add") { self.max += 2 }
ScrollableView {
ForEach(0..<self.max, id: \.self) { index in
Text("Hello \(index)")
.frame(width: geometry.size.width, height: 100)
.background(Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)))
}
}
}
}
}
}
class ScrollViewController<Content: View>: UIViewController {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad() // you need to call `super`
// self.view = UIView() // don't set `self.view`
addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(hostingController.view)
// scrollView.delegate = self // you're not currently using this delegate protocol, so we probably shouldn't set the delegate
// scrollView.scrollsToTop = true // these are the default values
// scrollView.isScrollEnabled = true
makeConstraints()
hostingController.didMove(toParent: self)
}
func makeConstraints() {
NSLayoutConstraint.activate([
// constraints for scroll view w/in main view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// define contentSize of scroll view relative to hosting controller's view
hostingController.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor)
])
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
}
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> ScrollViewController<Content> {
ScrollViewController(rootView: content())
}
func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = content()
viewController.hostingController.view.setNeedsUpdateConstraints()
}
}
I have a function in my UIViewControllers to setup a UINavigationBar which is repeated in many functions. I want to create the navigation bar in an extension but I want to set the title text and a cart label in each function. How can I do this? I think the answer is to use protocols but I'm not sure how.
Here is my extension
extension UIViewController {
func shoppingBagButtonTouched(button: UIButton) {
-----
}
func closeView() {
dismiss(animated: true, completion: nil)
}
func setupNavigationHeader(showCart: Bool? = true) {
let navigationBar: UINavigationBar = {
let navBar = UINavigationBar(frame: CGRect(0, 0, self.view.frame.size.width, Constants.HEADER_HEIGHT))
return navBar
}()
let navigationItem = UINavigationItem()
self.automaticallyAdjustsScrollViewInsets = false
UINavigationBar.appearance().barTintColor = .red
UINavigationBar.appearance().tintColor = .white
let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 1
label.text = "not set"
label.textColor = .white
return label
}()
let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let menuBtn = UIBarButtonItem(image: UIImage(named: "closeNav"), style: .plain, target: self, action: #selector(self.closeView))
navigationItem.leftBarButtonItems = [fixedSpace, menuBtn]
navigationBar.items = [navigationItem]
if let showCart = showCart {
let cartCountLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: -0, width: 20, height: 20))
label.textAlignment = .center
label.layer.cornerRadius = label.bounds.size.height / 2
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.textColor = .white
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
return label
}()
let shoppingBagButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 22, height: 22))
button.setBackgroundImage(UIImage(named: "shopping_bag"), for: .normal)
return button
}()
let rightBarButtonItem = UIBarButtonItem(customView: shoppingBagButton)
navigationItem.setRightBarButtonItems([rightBarButtonItem], animated: true)
shoppingBagButton.addTarget(self, action: #selector(shoppingBagButtonTouched(button:)), for: .touchUpInside)
shoppingBagButton.addSubview(cartCountLabel)
cartCountLabel.anchorCenterXToSuperview()
cartCountLabel.anchorCenterYToSuperview(constant: 2)
}
navigationBar.addSubview(titleLabel)
view.addSubview(navigationBar)
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -10).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
titleLabel.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 90).isActive = true
}
}
In each controller I have code like this to set the title label and cart label but it doesn't work when I create the nav bar in an extension.
var titleLabelText: String = "title not set"
var cartCount: String? {
didSet {
cartCountLabel.text = cartCount
}
}
func getCartCount() {
ServerUtility.getCartCountApi { (cartCount) in
if let count = cartCount as Int? {
if count > 0 {
self.cartCount = "\(count)"
}
} else {
self.cartCount = "0"
}
}
}
You are probably best off creating a base class extending UIViewController something like this:
class BaseViewController: UIViewController {
let cartCountLabel: UILabel = {
let label = UILabel(frame: CGRect(x: 0, y: -0, width: 20, height: 20))
label.textAlignment = .center
label.layer.cornerRadius = label.bounds.size.height / 2
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.textColor = .white
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
return label
}()
func shoppingBagButtonTouched(button: UIButton) {
-----
}
func closeView() {
dismiss(animated: true, completion: nil)
}
func setupNavigationHeader(showCart: Bool? = true) {
let navigationBar: UINavigationBar = {
let navBar = UINavigationBar(frame: CGRect(0, 0, self.view.frame.size.width, Constants.HEADER_HEIGHT))
return navBar
}()
let navigationItem = UINavigationItem()
self.automaticallyAdjustsScrollViewInsets = false
UINavigationBar.appearance().barTintColor = .red
UINavigationBar.appearance().tintColor = .white
let titleLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .clear
label.layer.masksToBounds = true
label.minimumScaleFactor = 10/UIFont.labelFontSize
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 1
label.text = "not set"
label.textColor = .white
return label
}()
let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let menuBtn = UIBarButtonItem(image: UIImage(named: "closeNav"), style: .plain, target: self, action: #selector(self.closeView))
navigationItem.leftBarButtonItems = [fixedSpace, menuBtn]
navigationBar.items = [navigationItem]
if let showCart = showCart {
let shoppingBagButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 22, height: 22))
button.setBackgroundImage(UIImage(named: "shopping_bag"), for: .normal)
return button
}()
let rightBarButtonItem = UIBarButtonItem(customView: shoppingBagButton)
navigationItem.setRightBarButtonItems([rightBarButtonItem], animated: true)
shoppingBagButton.addTarget(self, action: #selector(shoppingBagButtonTouched(button:)), for: .touchUpInside)
shoppingBagButton.addSubview(cartCountLabel)
cartCountLabel.anchorCenterXToSuperview()
cartCountLabel.anchorCenterYToSuperview(constant: 2)
}
navigationBar.addSubview(titleLabel)
view.addSubview(navigationBar)
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: -10).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
titleLabel.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 90).isActive = true
}
}
Then you can derive all your view controller instances from the base class like this:
class MyViewController:BaseViewController {
var titleLabelText: String = "title not set"
var cartCount: String? {
didSet {
cartCountLabel.text = cartCount
}
}
func getCartCount() {
ServerUtility.getCartCountApi { (cartCount) in
if let count = cartCount as Int? {
if count > 0 {
self.cartCount = "\(count)"
}
} else {
self.cartCount = "0"
}
}
}
}
I have not verified the above code by running it and so it might need a bit of tweaking :)
I have been dealing with JSON for a month now , getting better day by day. Here is a complex jigsaw I have been circling around. JSON return I get is like this :
{
"main_content": [
{
"product_title": "product 3",
"description": "",
"regular_price": "320",
"sale_price": "",
"product_currency": "$",
"size": [
{
"size_data": "L"
},
{
"size_data": "S"
}
],
"color": [
{
"color_data": "RED"
},
{
"color_data": "WHITE"
}
],
"gallery": [
{
"guid": "http://xxx/wp-content/uploads/2016/11/catagory1.jpg"
},
{
"guid": "http://xxx/wp-content/uploads/2016/11/catagory3.jpg"
}
]
}
]
}
Now , here product_title , description , regular_price , sale_price and product_currency will be outside of array as you can see. For size & color I need to fetch as array outside of my CollectionView , I dunno how to iterate through the array outside of a collectionView or tableView, coz in those I got an indexpath to iterate but I dunno what to do outside a collectionView or tableView. Finally My Image slider will be inside CollectionView therefore gallery items needs to be inside that. Here is visual image of my respective page.
Now here is the POST call using Alamofire in my view
import Foundation
import Alamofire
import SwiftyJSON
//Error could happen for these reason
enum ProductDetailsManagerError: Error {
case network(error: Error)
case apiProvidedError(reason: String)
case authCouldNot(reason: String)
case authLost(reason: String)
case objectSerialization(reason: String)
}
// APIManager Class
class ProductDetailsManager {
// THE RIGHT WAY A.K.A. "THE ONE LINE SINGLETON (NOW WITH PROOF!")
static let sharedInstance = ProductDetailsManager()
func printPublicGists(parameter: [String:Any]? , completionHandler: #escaping (Result<[ProductDetailsJSON]>) -> Void) {
let url: String = "http://xxx/wp-api/products/get_product_informations/"
Alamofire.request(url, method: .post, parameters: parameter, encoding: URLEncoding.default, headers: nil)
.responseJSON { (response) in
guard response.result.error == nil else {
print(response.result.error!)
return
}
guard let value = response.result.value else {
print("no string received in response when swapping oauth code for token")
return
}
print(value)
}
}
func fetchPublicGists(parameter: [String:Any]? , completionHandler: #escaping (Result<[ProductDetailsJSON]>) -> Void) {
let url: String = "http://xxx/wp-api/products/get_product_informations/"
Alamofire.request(url, method: .post, parameters: parameter, encoding: URLEncoding.default, headers: nil)
.responseJSON { response in
let result = self.gistArrayFromResponse(response: response)
completionHandler(result)
}
}
// Download Image from URL
func imageFrom(urlString: String, completionHandler: #escaping (UIImage?, Error?) -> Void) {
let _ = Alamofire.request(urlString)
.response { dataResponse in
// use the generic response serializer that returns Data
guard let data = dataResponse.data else {
completionHandler(nil, dataResponse.error)
return
}
let image = UIImage(data: data)
completionHandler(image, nil)
}
}
//gistArrayFromResponse function
private func gistArrayFromResponse(response: DataResponse<Any>) -> Result<[ProductDetailsJSON]> {
// For Network Error
guard response.result.error == nil else {
print(response.result.error!)
return .failure(RueDu8APIManagerError.network(error: response.result.error!))
}
// JSON Serialization Error, make sure we got JSON and it's an array
guard let jsonArray = response.result.value else {
print("did not get array of homeFeed object as JSON from API")
return .failure(RueDu8APIManagerError.objectSerialization(reason: "Did not get JSON dictionary in response"))
}
//turn JSON into gists
//let gistss = jsonArray.flatMap { HomeFeedJSON(items: $0) }
var gists = [ProductDetailsJSON]()
let jsonR = JSON(jsonArray)
let main_content = jsonR["main_content"].array
for item in main_content! {
gists.append(ProductDetailsJSON(items: item))
}
return .success(gists)
}//gistArrayFromResponse() function ends here
}
here is my model class
import Foundation
import SwiftyJSON
class ProductDetailsJSON {
var _product_title: String?
var _description: String?
var _regular_price: String?
var _sale_price: String?
var _product_currency: String?
var _size: String?
var _color: String?
var _image: URL?
init(items: JSON){
self._product_title = items["product_title"].stringValue
self._description = items["description"].stringValue
self._regular_price = items["regular_price"].stringValue
self._sale_price = items["sale_price"].stringValue
self._product_currency = items["product_currency"].stringValue
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
self._size = itemsIMG["size_data"].stringValue
}
let colorData = items["color"].arrayValue
for itemsColor in colorData {
self._size = itemsColor["color_data"].stringValue
}
let galleryImg = items["gallery"].arrayValue
for image in galleryImg {
self._image = image["guid"].URL
}
}
var product_title: String {
if _product_title == nil {
_product_title = ""
}
return _product_title!
}
var description: String {
if _description == nil {
_description = ""
}
return _description!
}
var regular_price: String {
if _regular_price == nil {
_regular_price = ""
}
return _regular_price!
}
var sale_price: String {
if _sale_price == nil {
_sale_price = ""
}
return _sale_price!
}
var product_currency: String {
if _product_currency == nil {
_product_currency = ""
}
return _product_currency!
}
var product_color: String {
if _color == nil {
_color = ""
}
return _size!
}
var product_image: URL {
if _image == nil {
let myURL = "http://www.clker.com/cliparts/d/L/P/X/z/i/no-image-icon-hi.png"
let noImage: URL = URL(string: myURL)!
_image = noImage
}
return _image!
}
}
and here is my controller class where I am struggling to show the size , color and gallery items from JSON
import UIKit
import DropDown
import Alamofire
import SwiftyJSON
class ShopItemVC: UIViewController , UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var contentView: UIView!
#IBOutlet weak var selectedProductImg: UIImageView!
#IBOutlet weak var backgroundCardView1: UIView!
#IBOutlet weak var backgroundCardView2: UIView!
#IBOutlet weak var backgroundCardView3: UIView!
#IBOutlet weak var sizeBtn: NiceButton!
#IBOutlet weak var colorBtn: NiceButton!
#IBOutlet weak var productPrice: UILabel!
#IBOutlet weak var productTitle: UILabel!
// var Title = [ProductDetailsJSON]()
var product_id:Int? //got value from SpecificCatagoryVC
var product_detail = [ProductDetailsJSON]()
var reloadData = 0
let sizeDropDown = DropDown()
let colorDropDown = DropDown()
lazy var dropDowns: [DropDown] = {
return [
self.sizeDropDown,
self.colorDropDown
]
}()
let CatagoryPic = ["catagory1","catagory2","catagory3","catagory4","catagory5","catagory6","c atagory7"]
// let CatagoryPicture = [ProductDetailsJSON]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
sizeBtn.layer.borderWidth = 1.2
sizeBtn.layer.borderColor = UIColor.black.cgColor
colorBtn.layer.borderWidth = 1.2
colorBtn.layer.borderColor = UIColor.black.cgColor
backgroundCardView1.backgroundColor = UIColor.white
backgroundCardView1.layer.cornerRadius = 5.0
backgroundCardView1.layer.masksToBounds = false
backgroundCardView1.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView1.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView1.layer.shadowOpacity = 0.8
backgroundCardView2.backgroundColor = UIColor.white
backgroundCardView2.layer.cornerRadius = 5.0
backgroundCardView2.layer.masksToBounds = false
backgroundCardView2.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView2.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView2.layer.shadowOpacity = 0.8
backgroundCardView3.backgroundColor = UIColor.white
backgroundCardView3.layer.cornerRadius = 5.0
backgroundCardView3.layer.masksToBounds = false
backgroundCardView3.layer.shadowColor = UIColor.black.withAlphaComponent(0.2).cgColor
backgroundCardView3.layer.shadowOffset = CGSize(width: 0, height: 0)
backgroundCardView3.layer.shadowOpacity = 0.8
setupDropDowns()
}
override func viewDidAppear(_ animated: Bool) {
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height + 40)
loadGists(parameter: ["product_id":product_id ?? 0])
}
func setupDropDowns() {
setupSizeDropDown()
setupColorDropDown()
}
func setupSizeDropDown() {
sizeDropDown.anchorView = sizeBtn
sizeDropDown.bottomOffset = CGPoint(x: 0, y: sizeBtn.bounds.height)
// You can also use localizationKeysDataSource instead. Check the docs.
sizeDropDown.dataSource = [
"XXL",
"XL",
"L",
"M",
"S"
]
// Action triggered on selection
sizeDropDown.selectionAction = { [unowned self] (index, item) in
self.sizeBtn.setTitle(item, for: .normal)
print(item)
}
}
func setupColorDropDown() {
colorDropDown.anchorView = colorBtn
colorDropDown.bottomOffset = CGPoint(x: 0, y: colorBtn.bounds.height)
// You can also use localizationKeysDataSource instead. Check the docs.
colorDropDown.dataSource = [
"Red",
"Blue",
"White",
"Purple",
"Pink"
]
// Action triggered on selection
colorDropDown.selectionAction = { [unowned self] (index, item) in
self.colorBtn.setTitle(item, for: .normal)
print(item)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loadGists(parameter: [String:Any]?) {
ProductDetailsManager.sharedInstance.fetchPublicGists(parameter: parameter) {
(result) in
guard result.error == nil else {
self.handleLoadGistsError(result.error!)
return
}
if let fetchedGists = result.value {
self.product_detail = fetchedGists
}
self.reloadData = 1
self.collectionView?.reloadData()
}
}
func handleLoadGistsError(_ error: Error) { }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return CatagoryPic.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ShopItemCell", for: indexPath) as! ShopItemCell
if reloadData == 1 {
let myProduct = self.product_detail[indexPath.row]
self.productTitle.text = myProduct.product_title
}
cell.shopItemPic.image = UIImage(named: CatagoryPic[(indexPath as NSIndexPath).row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// here you know which item is selected by accessing indexPath.item property, for example:
let selectedImage = UIImage(named: CatagoryPic[(indexPath as NSIndexPath).item])
selectedProductImg.image = selectedImage
}
#IBAction func sizeBtnPressed(_ sender: Any) {
sizeDropDown.show()
//print("size btn pressed")
}
#IBAction func colorBtnPressed(_ sender: Any) {
colorDropDown.show()
}
#IBAction func backBtn(_ sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
}
There are some other class where I am facing the same issue. Hope If I get this solution I will be able to solve those. Thanks in advance .
First thing to note is that you are returning the size value and not color in the product_color variable.
Also when you loop through the arrays in your JSON, you're setting the variable to only be the final value. For example here:
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
self._size = itemsIMG["size_data"].stringValue
}
The JSON is
"size": [
{
"size_data": "L"
},
{
"size_data": "S"
}
]
So _size is going to be set as "S" and "L" will never be assigned. I'd suggest changing _size, _color and _image to
var _sizes: [String] = []
var _colors: [String] = []
var _images: [String] = []
And then when looping through the JSON array:
let sizeData = items["size"].arrayValue
for itemsIMG in sizeData {
let size = itemsIMG["size_data"].stringValue
_sizes.append(size)
}
If I'm understanding correctly, you then want to update your dropdown data when you get the response with the gist.
sizeDropDown.dataSource = product_detail.product_sizes
Then the same for the other drop downs.
And create product_sizes, product_colors and product_images variables in the model similar sale_price and regular_price.
I am trying to preview the Images/Video/PDF in UICollectionViewCell (full screen).
for PDF I am trying to use the QuickLook framework.
Here is what I am trying to do,
CollectionView
CollectiViewCell
QLPreviewController.view as subview of CollectionView.contentView
and other cells will be simple UIImageViews.
So when I scroll right/left the touches are always consumed by QLPreviewController and I am unable to navigate to next cell.
I tried adding subview after adding qlpreviewcontroller.view (to intercept touches)
The Implementation follows:
class SampleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
return nil
}
}
class SCGalleryCollectionViewCell: UICollectionViewCell,UIScrollViewDelegate, QLPreviewControllerDataSource {
var quickLookController : PreviewController!
var scrollView : UIScrollView!
var imageView : SCImageView!
override init(frame: CGRect) {
super.init(frame: frame)
let sampleView = SampleView()
sampleView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(sampleView)
sampleView.fillHorizontally()
sampleView.fillVertically()
sampleView.backgroundColor = UIColor.greenColor()
contentView.bringSubviewToFront(sampleView)
quickLookController = PreviewController()
quickLookController.dataSource = self
// quickLookController.view.frame = CGRectMake(0, 0, 100, 100)
quickLookController.view.translatesAutoresizingMaskIntoConstraints = false
sampleView.addSubview(quickLookController.view)
quickLookController.view.fillVertically()
quickLookController.view.fillHorizontally()
quickLookController.view.userInteractionEnabled = false
quickLookController.view.gestureRecognizers = nil
}
}
class PreviewController: QLPreviewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.userInteractionEnabled = false
view.exclusiveTouch = false
view.multipleTouchEnabled = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return false
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
}
So my question is, how can I disable the swipe gesture on qlpreviewcontroller ?
Thanks in advance!!