I am responsible of a complete Swift 3 application and one of the crashes that occurs regularly is a SIGBUS signal that I can't understand at all:
Thread 0 Crashed:
0 libswiftCore.dylib 0x00000001009b4ac8 0x1007b8000 +2083528
1 LeadingBoards #objc PageView.prepareForReuse() -> () (in LeadingBoards) (PageView.swift:0) +1114196
2 LeadingBoards specialized ReusableContentView<A where ...>.reuseOrInsertView(first : Int, last : Int) -> () (in LeadingBoards) (ReusableView.swift:101) +1730152
3 LeadingBoards DocumentViewerViewController.reuseOrInsertPages() -> () (in LeadingBoards) (DocumentViewerViewController.swift:0) +1036080
4 LeadingBoards specialized DocumentViewerViewController.scrollViewDidScroll(UIScrollView) -> () (in LeadingBoards) (DocumentViewerViewController.swift:652) +1089744
5 LeadingBoards #objc DocumentViewerViewController.scrollViewDidScroll(UIScrollView) -> () (in LeadingBoards) +1028252
6 UIKit 0x000000018c2a68d4 0x18bf85000 +3283156
7 UIKit 0x000000018bfb2c08 0x18bf85000 +187400
8 UIKit 0x000000018c143e5c 0x18bf85000 +1830492
9 UIKit 0x000000018c143b4c 0x18bf85000 +1829708
10 QuartzCore 0x00000001890755dc 0x18906b000 +42460
11 QuartzCore 0x000000018907548c 0x18906b000 +42124
12 IOKit 0x00000001860d7b9c 0x1860d2000 +23452
13 CoreFoundation 0x0000000185e01960 0x185d3e000 +801120
14 CoreFoundation 0x0000000185e19ae4 0x185d3e000 +899812
15 CoreFoundation 0x0000000185e19284 0x185d3e000 +897668
16 CoreFoundation 0x0000000185e16d98 0x185d3e000 +888216
17 CoreFoundation 0x0000000185d46da4 0x185d3e000 +36260
18 GraphicsServices 0x00000001877b0074 0x1877a4000 +49268
19 UIKit 0x000000018bffa058 0x18bf85000 +479320
20 LeadingBoards main (in LeadingBoards) (AppDelegate.swift:13) +77204
21 libdyld.dylib 0x0000000184d5559c 0x184d51000 +17820
The logic behind that is the logic for reusing views in a scrollview, as described by Apple in a WWDC video (can't find the year and the video...):
PageView is a class that implement ReusableView and Indexed:
class PageView: UIView {
enum Errors: Error {
case badConfiguration
case noImage
}
enum Resolution: String {
case high
case low
static var emptyGeneratingTracker: [PageView.Resolution: Set<String>] {
return [.high:Set(),
.low:Set()]
}
/// SHOULD NOT BE 0
var quality: CGFloat {
switch self {
case .high:
return 1
case .low:
return 0.3
}
}
var JPEGQuality: CGFloat {
switch self {
case .high:
return 0.8
case .low:
return 0.25
}
}
var atomicWrite: Bool {
switch self {
case .high:
return false
case .low:
return true
}
}
var interpolationQuality: CGInterpolationQuality {
switch self {
case .high:
return .high
case .low:
return .low
}
}
var dispatchQueue: OperationQueue {
switch self {
case .high:
return DocumentBridge.highResOperationQueue
case .low:
return DocumentBridge.lowResOperationQueue
}
}
}
#IBOutlet weak var imageView: UIImageView!
// Loading
#IBOutlet weak var loadingStackView: UIStackView!
#IBOutlet weak var pageNumberLabel: UILabel!
// Error
#IBOutlet weak var errorStackView: UIStackView!
// Zoom
#IBOutlet weak var zoomView: PageZoomView!
fileprivate weak var bridge: DocumentBridge?
var displaying: Resolution?
var pageNumber = 0
override func layoutSubviews() {
super.layoutSubviews()
refreshImageIfNeeded()
}
func configure(_ pageNumber: Int, zooming: Bool, bridge: DocumentBridge) throws {
if pageNumber > 0 && pageNumber <= bridge.numberOfPages {
self.bridge = bridge
self.pageNumber = pageNumber
self.zoomView.configure(bridge: bridge, pageNumber: pageNumber)
} else {
throw Errors.badConfiguration
}
NotificationCenter.default.addObserver(self, selector: #selector(self.pageRendered(_:)), name: .pageRendered, object: bridge)
NotificationCenter.default.addObserver(self, selector: #selector(self.pageFailedRendering(_:)), name: .pageFailedRendering, object: bridge)
pageNumberLabel.text = "PAGE".localized + " \(pageNumber)"
if displaying == nil {
loadingStackView.isHidden = false
errorStackView.isHidden = true
}
if displaying != .high {
refreshImage()
}
if zooming {
startZooming()
} else {
stopZooming()
}
}
fileprivate func isNotificationRelated(notification: Notification) -> Bool {
guard let userInfo = notification.userInfo else {
return false
}
guard pageNumber == userInfo[DocumentBridge.PageNotificationKey.PageNumber.rawValue] as? Int else {
return false
}
guard Int(round(bounds.width)) == userInfo[DocumentBridge.PageNotificationKey.Width.rawValue] as? Int else {
return false
}
guard userInfo[DocumentBridge.PageNotificationKey.Notes.rawValue] as? Bool == false else {
return false
}
return true
}
func pageRendered(_ notification: Notification) {
guard isNotificationRelated(notification: notification) else {
return
}
if displaying == nil || (displaying == .low && notification.userInfo?[DocumentBridge.PageNotificationKey.Resolution.rawValue] as? String == Resolution.high.rawValue) {
refreshImage()
}
}
func pageFailedRendering(_ notification: Notification) {
guard isNotificationRelated(notification: notification) else {
return
}
if displaying == nil {
imageView.image = nil
loadingStackView.isHidden = true
errorStackView.isHidden = false
}
}
func refreshImageIfNeeded() {
if displaying != .high {
refreshImage()
}
}
fileprivate func refreshImage() {
let pageNumber = self.pageNumber
let width = Int(round(bounds.width))
DispatchQueue.global(qos: .userInitiated).async(execute: { [weak self] () in
do {
try self?.setImage(pageNumber, width: width, resolution: .high)
} catch {
_ = try? self?.setImage(pageNumber, width: width, resolution: .low)
}
})
}
func setImage(_ pageNumber: Int, width: Int, resolution: Resolution) throws {
if let image = try self.bridge?.getImage(page: pageNumber, width: width, resolution: resolution) {
DispatchQueue.main.async(execute: { [weak self] () in
if pageNumber == self?.pageNumber {
self?.imageView?.image = image
self?.displaying = resolution
self?.loadingStackView.isHidden = true
self?.errorStackView.isHidden = true
}
})
} else {
throw Errors.noImage
}
}
}
extension PageView: ReusableView, Indexed {
static func instanciate() -> PageView {
return UINib(nibName: "PageView", bundle: nil).instantiate(withOwner: nil, options: nil).first as! PageView
}
var index: Int {
return pageNumber
}
func hasBeenAddedToSuperview() { }
func willBeRemovedFromSuperview() { }
func prepareForReuse() {
NotificationCenter.default.removeObserver(self, name: .pageRendered, object: nil)
NotificationCenter.default.removeObserver(self, name: .pageFailedRendering, object: nil)
bridge = nil
imageView?.image = nil
displaying = nil
pageNumber = 0
zoomView?.prepareForReuse()
}
func prepareForRelease() { }
}
// MARK: - Zoom
extension PageView {
func startZooming() {
bringSubview(toFront: zoomView)
zoomView.isHidden = false
setNeedsDisplay()
}
func stopZooming() {
zoomView.isHidden = true
}
}
where ReusableView and Indexed are protocols defined that way :
protocol Indexed {
var index: Int { get }
}
protocol ReusableView {
associatedtype A
static func instanciate() -> A
func hasBeenAddedToSuperview()
func willBeRemovedFromSuperview()
func prepareForReuse()
func prepareForRelease()
}
// Make some func optionals
extension ReusableView {
func hasBeenAddedToSuperview() {}
func willBeRemovedFromSuperview() {}
func prepareForReuse() {}
func prepareForRelease() {}
}
ReusableContentView is a view that manage the view that are inserted, or reused. It's implemented depending of the containing view type :
class ReusableContentView<T: ReusableView>: UIView where T: UIView {
var visible = Set<T>()
var reusable = Set<T>()
...
}
extension ReusableContentView where T: Indexed {
/// To insert view using a range of ids
func reuseOrInsertView(first: Int, last: Int) {
// Removing no longer needed views
for view in visible {
if view.index < first || view.index > last {
reusable.insert(view)
view.willBeRemovedFromSuperview()
view.removeFromSuperview()
view.prepareForReuse()
}
}
// Removing reusable pages from visible pages array
visible.subtract(reusable)
// Add the missing views
for index in first...last {
if !visible.map({ $0.index }).contains(index) {
let view = dequeueReusableView() ?? T.instanciate() as! T // Getting a new page, dequeued or initialized
if configureViewWithIndex?(view, index) == true {
addSubview(view)
view.hasBeenAddedToSuperview()
visible.insert(view)
}
}
}
}
}
Witch is called by DocumentViewerViewController.reuseOrInsertPages(), triggered by scrollviewDidScroll delegate.
What can provoque my SIGBUS signal here? Is that the default implementation of func prepareForReuse() {} I use to make the protocol function optional? Any other ideas?
Of course, this crash is completly random and I wasn't able to reproduice it. I just receive crash reports about it from prod version of the app.
Thanks for your help !
For me it looks like something went wrong in PageView.prepareForReuse(). I'm not aware of the properties but from the prepareForReuse function it looks like your are accessing properties which maybe are #IBOutlets:
bridge = nil
imageView.image = nil
displaying = nil
pageNumber = 0
zoomView.prepareForReuse()
Could it be that imageView or zoomView are nil when you try to access them? If so, this could be the most simplistic fix:
func prepareForReuse() {
NotificationCenter.default.removeObserver(self, name: .pageRendered, object: nil)
NotificationCenter.default.removeObserver(self, name: .pageFailedRendering, object: nil)
bridge = nil
imageView?.image = nil
displaying = nil
pageNumber = 0
zoomView?.prepareForReuse()
}
Again, I am not sure about the implementation details of your PageView and I am only guessing this because it looks like you are instantiating it from a Nib and therefore my guess is you are using for example #IBOutlet weak var imageView: UIImageView!.
If for whatever reason this imageView becomes nil, accessing it will crash your app.
Related
I'm struggle with following challenge. I created table view with custom cell that contains switch. I wanna only one switch can be on i.e, for instance after launch I switched on 3rd switched and then I switched on 7th switch and thus the 3rd one is switched off and so on. I use rx + protocols for cell and don't understand all the way how to determine which switch was toggled. Previously I was going to use filter or map to look up in dataSource array which switch is on and somehow handle this, but now I messed up with it. I'm not sure it's possible without using table view delegate methods. Thanks a lot, hope someone could explain where I am wrong.
//My cell looks like this:
// CellViewModel implementation
import Foundation
import RxSwift
protocol ViewModelProtocol {
var bag:DisposeBag {get set}
func dispose()
}
class ViewModel:ViewModelProtocol {
var bag = DisposeBag()
func dispose() {
self.bag = DisposeBag()
}
}
protocol CellViewModelProtocol:ViewModelProtocol {
var isSwitchOn:BehaviorSubject<Bool> {get set}
}
class CellVM:ViewModel, CellViewModelProtocol {
var isSwitchOn: BehaviorSubject<BooleanLiteralType> = BehaviorSubject(value: false)
let internalBag = DisposeBag()
override init() {
}
}
//My Cell implementation
import UIKit
import RxSwift
import RxCocoa
class Cell:UITableViewCell {
static let identifier = "cell"
#IBOutlet weak var stateSwitch:UISwitch!
var vm:CellViewModelProtocol? {
didSet {
oldValue?.dispose()
self.bindUI()
}
}
var currentTag:Int?
var bag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
self.bindUI()
}
override func prepareForReuse() {
super.prepareForReuse()
self.bag = DisposeBag()
}
private func bindUI() {
guard let vm = self.vm else { return }
self.stateSwitch.rx.controlEvent(.valueChanged).withLatestFrom(self.stateSwitch.rx.value).observeOn(MainScheduler.asyncInstance).bind(to: vm.isSwitchOn).disposed(by: vm.bag)
}
}
//TableViewController implementation
import UIKit
import RxSwift
import RxCocoa
class TableViewController: UITableViewController {
private var dataSource:[CellViewModelProtocol] = []
var vm = TableViewControllerVM()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 70
self.tableView.rowHeight = UITableView.automaticDimension
self.bindUI()
}
private func bindUI() {
vm.dataSource.observeOn(MainScheduler.asyncInstance).bind { [weak self] (dataSource) in
self?.dataSource = dataSource
self?.tableView.reloadData()
}.disposed(by: vm.bag)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath) as! Cell
if cell.vm == nil {
cell.vm = CellVM()
}
return cell
}
}
class TableViewControllerVM:ViewModel {
var dataSource:BehaviorSubject<[CellViewModelProtocol]> = BehaviorSubject(value: [])
let internalBag = DisposeBag()
override init() {
super.init()
dataSource.onNext(createDataSourceOf(size: 7))
self.handleState()
}
private func createDataSourceOf(size:Int) -> [CellViewModelProtocol] {
var arr:[CellViewModelProtocol] = []
for _ in 0..<size {
let cell = CellVM()
arr.append(cell)
}
return arr
}
private func handleState() {
}
}
Maybe this code will help you:
extension TableViewController {
// called from viewDidLoad
func bind() {
let cells = (0..<7).map { _ in UUID() } // each cell needs an ID
let active = ReplaySubject<UUID>.create(bufferSize: 1) // tracks which is the currently active cell by ID
Observable.just(cells) // wrap the array in an Observable
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { _, element, cell in
// this subscription causes the inactive cells to turn off
active
.map { $0 == element }
.bind(to: cell.toggleSwitch.rx.isOn)
.disposed(by: cell.disposeBag)
// this subscription watches for when a cell is set to on.
cell.toggleSwitch.rx.isOn
.filter { $0 }
.map { _ in element }
.bind(to: active)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
}
}
Have a similar UI,so tested locally and it works.But not very neat code.
ProfileCellViewModel
struct ProfileCellViewModel {
// IMPORTANT!!!
var bibindRelay: BehaviorRelay<Bool>?
}
ProfileCell
final class ProfileCell: TableViewCell {
#IBOutlet weak var topLabel: Label!
#IBOutlet weak var centerLabel: Label!
#IBOutlet weak var bottomLabel: Label!
#IBOutlet weak var onSwitch: Switch!
public var vm: ProfileCellViewModel? {
didSet {
// IMPORTANT!!!
if let behaviorRelay = vm?.bibindRelay {
(onSwitch.rx.controlProperty(editingEvents: .valueChanged,
getter: { $0.isOn }) { $0.isOn = $1 } <-> behaviorRelay)
.disposed(by: self.rx.reuseBag)
}
}
}
}
ProfileViewModel
final class ProfileViewModel: ViewModel, ViewModelType {
struct Input {
let loadUserProfileStarted: BehaviorRelay<Void>
}
struct Output {
let userItems: BehaviorRelay<[ProfileCellViewModel]>
let chatRelay: BehaviorRelay<Bool>
let callRelay: BehaviorRelay<Bool>
}
let input = Input(loadUserProfileStarted: BehaviorRelay<Void>(value: ()))
let output = Output(userItems: BehaviorRelay<[ProfileCellViewModel]>(value: []),
chatRelay: BehaviorRelay<Bool>(value: false),
callRelay: BehaviorRelay<Bool>(value:false))
override init() {
super.init()
// IMPORTANT!!!
Observable.combineLatest(output.chatRelay,output.callRelay).pairwise().map { (arg0) -> Int in
let (pre, curr) = arg0
let preFlag = [pre.0,pre.1].filter { $0 == true }.count == 1
let currFlag = [curr.0,curr.1].filter { $0 == true }.count == 2
if preFlag && currFlag {
return [pre.0,pre.1].firstIndex(of: true) ?? 0
}
return -1
}.filter {$0 >= 0}.subscribe(onNext: { (value) in
[self.output.chatRelay,self.output.callRelay][value].accept(false)
}).disposed(by: disposeBag)
}
private func createProfileCellItems(user: User) -> [ProfileCellViewModel] {
// IMPORTANT!!!
let chatCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.chat(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.chatRelay)
// IMPORTANT!!!
let callCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.call(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.callRelay)
return [roleCellViewModel,
teamCellViewModel,
statusCellViewModel,
sinceCellViewModel,
chatCellViewModel,
callCellViewModel]
}
}
I mark the codes you should pay attention to with // IMPORTANT!!!
I am adding in some functionalities for this iOS swift matching card game and I need to check if the first 2 buttons flipped over match. They have four types of emojis for eight cards, which means there are 4 pairs of matches. I am having trouble finding out how to check if the cards match and when they match, I need the background color of the buttons to opaque (invisible). Everything else works except the empty if statement in the concentration class within the chooseCard function. That is where I need help.
Here's all the code so you can see whats related to what:
class ViewController: UIViewController {
lazy var game = Concentration(numberOfPairsOfCards: (cardButtons.count + 1) / 2)
var flipCount = 0 {
// Property Observer
didSet { flipLabel.text = "Flips: \(flipCount)" }
}
#IBOutlet weak var flipLabel: UILabel!
#IBOutlet var cardButtons: [UIButton]!
#IBAction func touchCard(_ sender: UIButton) {
flipCount+=1
if let cardNumber = cardButtons.firstIndex(of: sender) {
game.chooseCard(at: cardNumber)
updateViewFromModel()
} else {
print ("chosen card was not in cardButtons")
}
}
var emojiChoices = ["👻", "🎃", "🙏🏾", "🦆"]
var emoji = [Int:String]()
func emoji(for card: Card) -> String {
if emoji[card.identifier] == nil, emojiChoices.count > 0 {
let randomIndex = Int(arc4random_uniform(UInt32(emojiChoices.count)))
emoji[card.identifier] = emojiChoices.remove(at: randomIndex)
}
return emoji[card.identifier] ?? "?"
}
func updateViewFromModel() {
for index in cardButtons.indices {
let button = cardButtons[index]
let card = game.cards[index]
if card.isFaceUp {
button.setTitle(emoji(for: card), for: UIControl.State.normal)
button.backgroundColor = UIColor.white
}
else {
button.setTitle("", for: UIControl.State.normal)
button.backgroundColor = UIColor.orange
}
}
}
}
import Foundation
class Concentration {
var cards = [Card]()
func chooseCard(at index: Int) {
if cards[index].isFaceUp {
cards[index].isFaceUp = false
}
else {
cards[index].isFaceUp = true
}
if cards[index].isFaceUp && cards[index].isMatched {
}
}
init(numberOfPairsOfCards: Int) {
for _ in 0..<numberOfPairsOfCards {
let card = Card()
cards += [card,card]
}
//TODO: Shuffle cards
cards.shuffle()
}
}
import Foundation
struct Card {
var isMatched = false
var isFaceUp = false
var identifier: Int
static var identifierFactory = 0
static func getUniqueIdentifier() -> Int {
Card.identifierFactory += 1
return Card.identifierFactory
}
init() {
self.identifier = Card.getUniqueIdentifier()
}
}
In your concentration class you will check for faceUp card indexes
Change your Concentration from class to Struct
struct Concentration { // instead of class Concentration
private var indexOfFaceUpCard: Int? {
get {
let faceUpCardIndices = cards.indices.filter { cards[$0].isFaceUp }
return faceUpCardIndices.count == 1 ? faceUpCardIndices.first : nil
} set {
for index in cards.indices {
cards[index].isFaceUp = (index == newValue)
}
}
}
Then you have mutating chooseCard method
mutating func chooseCard(at index: Int)
In your chooseCard method you check for matching
if !cards[index].isMatched {
if let matchIndex = indexOfFaceUpCard, matchIndex != index {
if cards[matchIndex] == cards[index] {
cards[matchIndex].isMatched = true
cards[index].isMatched = true
}
cards[index].isFaceUp = true
} else {
indexOfFaceUpCard = index
}
}
So your method look like this
mutating func chooseCard(at index: Int) {
if !cards[index].isMatched {
if let matchIndex = indexOfFaceUpCard, matchIndex != index {
if cards[matchIndex] == cards[index] {
cards[matchIndex].isMatched = true
cards[index].isMatched = true
}
cards[index].isFaceUp = true
} else {
indexOfFaceUpCard = index
}
}
}
Update your card struct
struct Card: Hashable {
var isMatched = false
var isFaceUp = false
var identifier: Int
static var identifierFactory = 0
static func getUniqueIdentifier() -> Int {
Card.identifierFactory += 1
return Card.identifierFactory
}
static func ==(lhs: Card, rhs: Card) -> Bool {
return lhs.identifier == rhs.identifier
}
init() {
self.identifier = Card.getUniqueIdentifier()
}
}
I'm currently trying to develop a prototype for a quiz application.
The questions for the quiz are read out of a JSON file and the answers are generated within the program. Those two parts are then matched to each other within a Mediator class.
Displaying the question in my ViewController works perfectly fine for the first question. When trying to retrieve the second question the array that stores the question in the Mediator class is empty. That obviously is the case, because I'm re-entering the Mediator after going to my ViewController.
How can I store the data more efficiently?
If you need more information, please let me know. I'm really grateful for every suggestion I can get. I'm totally stuck at right now!
ViewController:
[...]
override func viewDidLoad() {
super.viewDidLoad()
setup(categoryType)
navigationBar.title = barTitle
QAMatcher.delegate = self
QAMatcher.getCategory(categoryType)
}
#objc func didChooseAnswer(_ sender: UIButton){
QAMatcher.nextQuestion()
updateUI()
}
func updateUI(){
if let label = questionLabel {
label.text = QAMatcher.getQuestion()
}
if let type = AnswerType(rawValue: QAMatcher.getAnswerType()) {
addAnswerSection(type)
}
}
func addAnswerSection(_ type: AnswerType) {
if let stackView = quizStackView, let answerSection = uiBuilder.getAnswerSection(answerType: type) {
stackView.addArrangedSubview(answerSection)
}
}
[...]
Mediator Class:
import Foundation
protocol Mediator {
func sendRequest (_ request: Request, sender: Sender)
}
protocol MediatorDelegate {
func didUpdate(mediator: QuestionAnswerMediator, _ array: [QuestionData])
}
enum Sender {
case answer
case question
}
class QuestionAnswerMediator: Mediator {
var delegate: MediatorDelegate?
var questionArray = [QuestionData]()
var answerArray = [String]()
var request = Request()
var questionNum = 0
func getCategory(_ category: CategoryType) {
request.categoryType = category
matchQuestionAndAnswers()
}
func sendRequest (_ request: Request, sender: Sender){
if sender == .question {
let array = QuestionManager.shared.send(category: request.categoryType!)
questionArray.append(contentsOf: array)
}
else if sender == .answer {
let array = AnswerManager.shared.send(request: request)
if array?.isEmpty == false {
answerArray = []
answerArray.append(contentsOf: array ?? ["default"])
}
}
}
func fetchAnswers(for type: String, question: String) {
request.answerType = AnswerType(rawValue: type)
request.question = question
sendRequest(request, sender: .answer)
}
func fetchQuestions (for category: CategoryType){
request.categoryType = category
sendRequest(request, sender: .question)
}
func matchQuestionAndAnswers(){
var i = 0
fetchQuestions(for: request.categoryType!)
for question in questionArray {
fetchAnswers(for: question.answerType, question: question.question)
if answerArray.isEmpty != true {
questionArray[i].answers = answerArray
}
i += 1
}
self.delegate?.didUpdate(mediator: self, questionArray)
}
func getQuestion() -> String {
return questionArray[questionNum].question
}
func getAnswers() -> [String] {
return questionArray[questionNum].answers!
}
func getAnswerType() -> String {
return questionArray[questionNum].answerType
}
func nextQuestion () {
if questionNum + 1 < questionArray.count {
questionNum += 1
}
else {
questionNum = 0
}
}
}
ok, that was hard ;) i had to create my own project file, because yours was still missing. After doing that i tried to compile and run...but you did not fill the assets so it crashed. After faking your assets...it runs. ;)
You already analyzed your problem correctly. You should never create a viewcontroller mutliple times if you just call him once. So i deleted this and you now only have one instance.
i routed the "didchooseAnswer" through your classes...but you still have some work to do: i had to give a frame to your view class, so fill that frame you need, i don't know what frame you need.
I just corrected it for MultipleChoiceView...you have to do it for the other views as well like PolarView etc...
i hope i have everything copied what i changed, if not, just tell me. but maybe then you should give me rights on your github project so that i can commit it there...
Quizviewcontroller:
import UIKit
import CoreLocation
import os
class QuizViewController: UIViewController {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var navigationBar: UINavigationItem!
#IBOutlet var mainView: UIView!
#IBOutlet weak var quizStackView: UIStackView!
var barTitle: String?
//variables for location services
var locationManager = CLLocationManager()
var locationData: LocationModel?
//variables for communication with Mediator
var QAMatcher = QuestionAnswerMediator()
var questions : [QuestionData]?
var categoryType: CategoryType!
var timer = Timer()
//Change UI depending on answer type
var uiBuilder = AnswerViewBuilder()
override func viewDidLoad() {
super.viewDidLoad()
setup(categoryType)
navigationBar.title = barTitle
QAMatcher.delegate = self
QAMatcher.getCategory(categoryType)
}
#objc func didChooseAnswer(_ sender: UIButton){
QAMatcher.nextQuestion()
updateUI()
}
}
//MARK: - Setup & updating UI
extension QuizViewController: MediatorDelegate {
func didUpdate(mediator: QuestionAnswerMediator, _: [QuestionData]) {
self.updateUI()
}
func setup(_ category: CategoryType?) {
switch category {
case .shortterm:
print("No function yet")
case .longterm:
print("No function yet")
case .problemsolving:
print("No function yet")
case .orientation:
locationManager.delegate = self
checkLocationServiceSupport()
locationManager.requestWhenInUseAuthorization()
gecodeCurrentLocation()
default:
let message: StaticString = M.Errors.invalidCategoryError
os_log(message, log: OSLog.default, type: .error)
}
}
func updateUI(){
if let label = questionLabel {
label.text = QAMatcher.getQuestion()
}
if let type = AnswerType(rawValue: QAMatcher.getAnswerType()) {
addAnswerSection(type)
}
}
func addAnswerSection(_ type: AnswerType) {
if let stackView = quizStackView, let answerSection = uiBuilder.getAnswerSection(answerType: type, didChooseAnswer: self.didChooseAnswer(_:)) {
stackView.addArrangedSubview(answerSection)
}
}
}
//MARK: - CLLocationManagerDelegate
extension QuizViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationManager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
let message: StaticString = M.Errors.locationDetectionError
os_log(message, log: OSLog.default, type: .error)
}
func checkLocationServiceSupport () {
if CLLocationManager.significantLocationChangeMonitoringAvailable() != true {
let alert = UIAlertController(title: M.Alters.locationAlertTitle, message: M.Alters.locationAlert, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
NSLog("The user acknowledge that location services are not available.")
}))
self.present(alert, animated: true, completion: nil)
self.dismiss(animated: true, completion: nil)
}
}
func gecodeCurrentLocation() {
locationManager.requestLocation()
if let lastLocation = self.locationManager.location {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(lastLocation) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?.first {
let city = placemark.locality!
let country = placemark.country!
self.locationData = LocationModel(city: city,country: country)
}
}
else {
let message: StaticString = M.Errors.locationDecodingError
os_log(message, log: OSLog.default, type: .error)
}
}
}
}
}
//MARK: - UITextFieldDelegate
//extension CategoryViewController: UITextFieldDelegate {
//
// func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
// if textField.text != "" {
// return true
// }
// else {
// textField.placeholder = "Enter your answer here"
// return false
// }
// }
//
// func textFieldDidEndEditing(_ textField: UITextField) {
// }
//
// func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// textField.endEditing(true)
// return true
// }
//}
MulitpleChoiceView
import UIKit
class MultipleChoiceView: UIStackView {
var button1 = UIButton()
var button2 = UIButton()
var button3 = UIButton()
var didChooseAnswer : ((UIButton)->())?
init(frame: CGRect, didChooseAnswer: #escaping (UIButton)->()) {
super.init(frame: frame)
self.didChooseAnswer = didChooseAnswer
self.addAnswerView()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addAnswerView() {
setupStackView()
costumeButtons()
}
func setupStackView() {
self.alignment = .fill
self.distribution = .fillEqually
self.spacing = 10
self.axis = .vertical
}
func costumeButtons() {
let buttons = [button1 , button2, button3]
let dimensions = CGRect(x: 0, y: 0, width: 120, height: 20)
let color = #colorLiteral(red: 0.7294117647, green: 0.7450980392, blue: 1, alpha: 1)
for button in buttons {
button.frame = dimensions
button.backgroundColor = color
button.layer.cornerRadius = button.frame.size.height / 4
button.isUserInteractionEnabled = true
button.addTarget(self, action: M.Selector.buttonAction, for: .touchUpInside)
self.addArrangedSubview(button)
}
}
#objc func didChooseAnswer(_ sender: UIButton) {
if let didChooseAnswer = didChooseAnswer {
didChooseAnswer(sender)
}
}
}
AnswerViewBuilder
import UIKit
class AnswerViewBuilder {
func getAnswerSection(answerType: AnswerType, didChooseAnswer: #escaping (UIButton)->()) -> UIView?{
switch (answerType) {
case .multipleChoice:
return MultipleChoiceView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), didChooseAnswer: didChooseAnswer)
case .textField:
return TextFieldView()
case .polarQuestion:
return PolarQuestionView()
case .ranking:
return MultipleChoiceView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), didChooseAnswer: didChooseAnswer)
default:
print("Answer section could not be created.")
return nil
}
}
}
In my struct Main Model (MainModel.swift):
private var charLevel: Int = 1
mutating func setCharLevel(_ value: Int) {
charLevel = value
}
var getCharLevel: Int? {
get {
return charLevel
}
}
In my firstViewController.swift:
private var MyModel = MainModel()
let sb = UIStoryBoard(name: "Main", bundle: nil)
#IBAction func addLevel(_ sender: UIButton){
if let secondVC = sb.instantiateViewController(withIdentifier: "SecondVC") as? SecondViewController {
self.present(secondVC, animated: true, completion: nil)
let charNewLevel = MyModel.getCharLevel! + 1
MyModel.setCharlevel(charNewLevel)
}
}
The SecondVC has only one label that shows charLevel from MainModel.swift and a button with self.dismiss that returns to firstViewController. So it just goes in a loop / circle. My problem is the level only goes to 2, even if I do 3+ runs, what am I missing? I would like it to increment by 1 every run (1st run: 2, 2nd run: 3, 3rd run: 4 and so on). Thank you from a learning student.
EDIT:
firstViewController -> ViewController (original name)
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var battleDescription: UILabel!
#IBOutlet weak var heroHealthLabel: UILabel!
#IBOutlet weak var enemyHealthLabel: UILabel!
#IBOutlet weak var byBattleDescription: UILabel!
private var QuestModel = MainModelQuest()
let sb = UIStoryboard(name: "Main", bundle: nil)
override func viewDidLoad() {
super.viewDidLoad()
QuestModel.setHeroHealth(1000)
QuestModel.setHeroMaxHealth(1000)
QuestModel.setEnemyHealth(150)
if let initialTempHeroHealth = QuestModel.getHeroHealth {
checkHeroLabel = initialTempHeroHealth
}
if let initialTempEnemyHealth = QuestModel.getEnemyHealth {
checkEnemyLabel = initialTempEnemyHealth
}
}
var checkHeroLabel: Int {
get {
return Int(heroHealthLabel.text!)!
} set {
heroHealthLabel.text = String(newValue)
}
}
var checkEnemyLabel: Int {
get {
return Int(enemyHealthLabel.text!)!
} set {
enemyHealthLabel.text = String(newValue)
}
}
#IBAction func attackOption(_ sender: UIButton) {
let tempHeroRandomX = arc4random_uniform(5) + 1
let tempHeroRandomY = arc4random_uniform(5) + 1
let tempEnemyRandomX = arc4random_uniform(5) + 1
let tempEnemyRandomY = arc4random_uniform(5) + 1
if tempHeroRandomX == tempHeroRandomY {
battleDescription.text = "Hero's attacked missed."
QuestModel.setHeroDamage(0)
} else {
if let chosenAttack = sender.currentTitle {
switch chosenAttack {
case "Attack1":
// 30 - 40
let tempHeroRandAttack = arc4random_uniform(11) + 30
QuestModel.setHeroDamage(Int(tempHeroRandAttack))
case "Attack2":
// 20 - 30
let tempHeroRandAttack = arc4random_uniform(11) + 20
QuestModel.setHeroDamage(Int(tempHeroRandAttack))
case "Attack3":
// 10 - 20
let tempHeroRandAttack = arc4random_uniform(11) + 10
QuestModel.setHeroDamage(Int(tempHeroRandAttack))
case "Attack4":
// 1 - 10
let tempHeroRandAttack = arc4random_uniform(10) + 1
QuestModel.setHeroDamage(Int(tempHeroRandAttack))
default:
break
}
}
}
if tempEnemyRandomX == tempEnemyRandomY {
byBattleDescription.text = "The enemy's attacked missed"
QuestModel.setEnemyDamage(0)
} else {
let tempEnemyRandAttack = arc4random_uniform(11) + 10
QuestModel.setEnemyDamage(Int(tempEnemyRandAttack))
}
if QuestModel.getEnemyDamage! > QuestModel.getHeroHealth! {
QuestModel.setHeroHealth(0)
if let heroKilledVC = sb.instantiateViewController(withIdentifier: "HeroFaintedVC") as? ThirdViewController {
self.present(heroKilledVC, animated: true, completion: nil)
}
} else {
let heroDamage = QuestModel.getHeroHealth! - QuestModel.getEnemyDamage!
QuestModel.setHeroHealth(heroDamage)
}
if QuestModel.getHeroDamage! > QuestModel.getEnemyHealth! {
QuestModel.setEnemyHealth(0)
if let enemyKilledVC = sb.instantiateViewController(withIdentifier: "EnemyFaintedVC") as? SecondViewController {
self.present(enemyKilledVC, animated: true, completion: nil)
let checkTotalExpi = QuestModel.getHeroExpi! + 30
if checkTotalExpi >= QuestModel.getHeroMaxExpi! {
let heroNewLevel = QuestModel.getHeroLevel! + 1
QuestModel.setHeroLevel(heroNewLevel)
let newHeroMaxExpi = QuestModel.getHeroMaxExpi! * 2
QuestModel.setHeroMaxExpi(newHeroMaxExpi)
QuestModel.setHeroExpi(0)
}
enemyKilledVC.infoObject = QuestModel.getHeroLevel
}
} else {
let enemyDamage = QuestModel.getEnemyHealth! - QuestModel.getHeroDamage!
QuestModel.setEnemyHealth(enemyDamage)
}
if QuestModel.getHeroDamage! > 0 {
if QuestModel.getEnemyHealth! <= 0 {
battleDescription.text = "Hero dealt \(QuestModel.getHeroDamage!) damage and the enemy fainted."
} else {
if let attackName = sender.currentTitle {
battleDescription.text = "Hero used " + attackName + " and dealt \(QuestModel.getHeroDamage!) damage."
}
}
}
if QuestModel.getEnemyDamage! > 0 {
if QuestModel.getHeroHealth! <= 0 {
byBattleDescription.text = "the enemy dealt \(QuestModel.getEnemyDamage!) and the hero fainted."
} else {
byBattleDescription.text = "Enemy attacked and dealt \(QuestModel.getEnemyDamage!) damage."
checkHeroLabel = QuestModel.getHeroHealth!
checkEnemyLabel = QuestModel.getEnemyHealth!
}
}
}
}
MainModel -> MainModelQuest (original name)
import Foundation
struct MainModelQuest {
// Hero Properties
private var heroHealth: Int?
private var heroMaxHealth: Int?
private var heroMana: Int?
private var heroMaxMana: Int?
private var heroStamina: Int?
private var heroMaxStamina: Int?
private var heroLevel: Int = 1
private var heroDamage: Int?
private var heroMaxDamage: Int? // Not yet used
private var heroExpi: Int = 25
private var heroMaxExpi: Int = 30
private var heroGold: Int? // Not yet used
private var heroGainedGold: Int? // Not yet used
// Enemy Properties
private var enemyHealth: Int?
private var enemyDamage: Int?
private var enemyLevel: Int? // Not yet used
mutating func setHeroHealth(_ value: Int) {
heroHealth = value
}
mutating func setHeroMaxHealth(_ value: Int) {
heroMaxHealth = value
}
mutating func setHeroLevel(_ value: Int) {
heroLevel = value
}
mutating func setHeroDamage(_ value: Int) {
heroDamage = value
}
mutating func setHeroExpi(_ value: Int) {
heroExpi = value
}
mutating func setHeroMaxExpi (_ value: Int) {
heroMaxExpi = value
}
mutating func setEnemyHealth(_ value: Int) {
enemyHealth = value
}
mutating func setEnemyDamage(_ value: Int) {
enemyDamage = value
}
var getHeroHealth: Int? {
get {
return heroHealth
}
}
var getHeroMaxHealth: Int? {
get {
return heroMaxHealth
}
}
var getHeroLevel: Int? {
get {
return heroLevel
}
}
var getHeroDamage: Int? {
get {
return heroDamage
}
}
var getHeroExpi: Int? {
get {
return heroExpi
}
}
var getHeroMaxExpi: Int? {
get {
return heroMaxExpi
}
}
var getEnemyHealth: Int? {
get {
return enemyHealth
}
}
var getEnemyDamage: Int? {
get {
return enemyDamage
}
}
}
SeconfViewController
import UIKit
class SecondViewController: UIViewController {
var infoObject: Int? {
didSet {
enemyDefeatedObject.text = "Lv. " + String(infoObject!)
}
}
#IBOutlet weak var enemyDefeatedObject: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
// Return to map
#IBAction func BacktoMapVC(_ sender: Any) {
let sb = UIStoryboard(name: "Main", bundle: nil)
if let mapVC = sb.instantiateViewController(withIdentifier: "MapVC") as? MapViewController {
self.present(mapVC, animated: true, completion: nil)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I understand that this may be a little bulk of code but this completes what I have been practicing so far, I may have incorrect conventions here but you are more than welcome to correct me on those point. My problem is upon running the program the 1st time, it gets correct level which is 2, 2nd run and conceding runs after are still two. Any suggestion is welcome. Thanks you.
I am using library SJSegmentedViewController for my project, github link to pod
Problem:
I have main view controller (FilterVC) on which I have a button "APPLY", on its action I want to access an array stored in another viewcontroller (FilterSkillVC), I am doing this using delegation, but still what I get is an empty instance
UPDATED
MY FilterVC code
import UIKit
import SJSegmentedScrollView
protocol FilterVCDelegate {
func btnApply()
}
class FilterVC: UIViewController {
var selectedSegment: SJSegmentTab?
var segmentedVC : SJSegmentedViewController?
var vcDelegate : FilterVCDelegate?
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
segmentViewInitialization()
}
#IBAction func btnApply(_ sender: Any) {
vcDelegate?.btnApply()
}
}
extension FilterVC {
var titles: [String] {
return [SegmentTitles.skillSet.rawValue,
SegmentTitles.cuisines.rawValue,
SegmentTitles.others.rawValue ]
}
var tabs: [String] {
return [StoryboardId.skillSet.rawValue,
StoryboardId.skillSet.rawValue,
StoryboardId.others.rawValue ]
}
func segmentViewInitialization() {
segmentedVC = CSSegment.setupTabs(storyboard: self.storyboard, tabs: tabs, titles: titles) as? SJSegmentedViewController
segmentedVC?.delegate = self
segmentedVC?.selectedSegmentViewHeight = 2.0
segmentedVC?.segmentTitleColor = .white
segmentedVC?.selectedSegmentViewColor = AppColor.secondary.value
segmentedVC?.segmentBackgroundColor = AppColor.primary.value
segmentedVC?.segmentViewHeight = 64.0
segmentedVC?.segmentShadow = SJShadow.light()
segmentedVC?.segmentTitleFont = AppFont.avenirMedium.size(14.0)
containerView.addSubview((segmentedVC?.view)!)
segmentedVC?.view.frame = containerView.bounds
}
}
extension FilterVC: SJSegmentedViewControllerDelegate {
func didMoveToPage(_ controller: UIViewController, segment: SJSegmentTab?, index: Int) {
if index != tabs.count-1 {
let filterVC = controller as! FilterSkillVC
filterVC.updateCurrentHeader(currentTab:SegmentTitles(rawValue: titles[index])!)
}
if selectedSegment != nil {
selectedSegment?.titleColor(.white)
}
if (segmentedVC?.segments.count)! > 0 {
selectedSegment = segmentedVC?.segments[index]
selectedSegment?.titleColor(AppColor.secondary.value)
}
}
}
My Skill VC code
import UIKit
class FilterSkillVC: UIViewController {
#IBOutlet var tblView: UITableView!
var instance = FilterVC()
lazy var arraySkills = [JSTags]()
lazy var arrayCuisines = [JSTags]()
var arrayID = [String]()
var currentHeader: SegmentTitles = .skillSet
override func viewDidLoad() {
super.viewDidLoad()
apiSkillCall()
apiCuisineCall()
registerCell(cellId: .filterListCell, forTableView: tblView)
tblView.tableFooterView = UIView()
// let instance = FilterVC()
instance.vcDelegate = self
}
func updateCurrentHeader(currentTab : SegmentTitles){
currentHeader = currentTab
tblView.reloadData()
}
//MARK: ----- Custom Methods
func countForHeader() -> NSInteger {
switch currentHeader {
case .skillSet:
return arraySkills.count
case .cuisines:
return arrayCuisines.count
default:
return 0
}
}
func titleForHeader(_ index: NSInteger) -> (name: String?, obj: AnyObject?) {
switch currentHeader {
case .skillSet:
return (name: arraySkills[index].name, obj: arraySkills[index])
case .cuisines:
return (name: arrayCuisines[index].name, obj: arrayCuisines[index])
default:
return (name: nil, obj: nil)
}
}
//MARK: ----- Handle Response Methods
func handleSkillsResponse(response: Response) {
switch response{
case .success(let response):
if let skills = response as? [JSTags] {
self.arraySkills = skills
}
case .failure(let str):
Alerts.shared.show(alert: .oops, message: /str, type: .error)
case .dataNotExist(let str):
Alerts.shared.show(alert: .oops, message: str, type: .info)
}
tblView.reloadData()
}
func handleCuisineResponse(response: Response) {
switch response{
case .success(let response):
if let cuisines = response as? [JSTags] {
self.arrayCuisines = cuisines
tblView.reloadData()
}
case .failure(let str):
Alerts.shared.show(alert: .oops, message: /str, type: .error)
case .dataNotExist(let str):
Alerts.shared.show(alert: .oops, message: str, type: .info)
}
}
//MARK: API Methods
func apiSkillCall() {
APIManager.shared.request(with: ProfileEndPoint.fetchSkills()) { (response) in
self.handleSkillsResponse(response: response)
}
}
func apiCuisineCall() {
APIManager.shared.request(with: ProfileEndPoint.fetchCuisines()) { (response) in
self.handleCuisineResponse(response: response)
}
}
}
extension FilterSkillVC : UITableViewDelegate, UITableViewDataSource, FilterListCellDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return countForHeader()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.filterListCell.rawValue) as! FilterListCell
let filter = titleForHeader(indexPath.row)
cell.lblFilterLabel.text = filter.name
//Mark: Cell delegate
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
//Mark: FilterCellDelegate Method
func buttonTapped(cell: FilterListCell) {
if let indexPath = self.tblView.indexPath(for: cell) {
print("Button tapped on row \(indexPath.row)")
if currentHeader == .skillSet {
arraySkills[indexPath.row].isSelected = !arraySkills[indexPath.row].isSelected
}
else {
arrayCuisines[indexPath.row].isSelected = !arrayCuisines[indexPath.row].isSelected
}
}
}
}
extension FilterSkillVC : FilterVCDelegate {
func btnApply() {
for object in arraySkills {
if object.isSelected {
arrayID.append((object.id) ?? "")
}
}
for object in arrayCuisines {
if object.isSelected {
arrayID.append((object.id) ?? "")
}
}
}
}
You are losing the reference to the instance as soon as the viewDidLoad method is completed.
Make instance a global Variable.
Like so :
import UIKit
class FilterSkillVC: UIViewController {
#IBOutlet var tblView: UITableView!
var instance = FilterVC() //New line added here.
lazy var arraySkills = [JSTags]()
lazy var arrayCuisines = [JSTags]()
var arrayID = [String]()
var currentHeader: SegmentTitles = .skillSet
override func viewDidLoad() {
super.viewDidLoad()
apiSkillCall()
apiCuisineCall()
registerCell(cellId: .filterListCell, forTableView: tblView)
tblView.tableFooterView = UIView()
//let instance = FilterVC() //Commented this.
instance.vcDelegate = self
}
More updates :
In the didMoveToPage method, you are getting a reference to a FilterVC (from a storyboard ??), now this instance of FilterVC is different from the instance of filterVC we created.
Please add this change and try :
func didMoveToPage(_ controller: UIViewController, segment: SJSegmentTab?, index: Int) {
if index != tabs.count-1 {
let filterVC = controller as! FilterSkillVC
filterVC.updateCurrentHeader(currentTab:SegmentTitles(rawValue: titles[index])!)
self.vcDelegate = filterVC // <== Updated this line.
}