I'm having problems with a fairly basic task while experimenting with Apple Watch development in Swift. I recently received what appeared to be a perfect answer to the problem in Obj C but I can't seem to translate it.
I have one Interface controller that contains a table with multiple rows of the same row controller. Each row contains two buttons. When the button is tapped it's background image changes, but another function needs to be called in the interface controller class, and this function will need to access class-level variables in the interface controller class.
The Table is wired to the interface controller. And the buttons are wired to the row controller class. Here is my simplified Swift code. Thanks for having a look!
/// MY INTERFACE CLASS ///
class MainBoard: WKInterfaceController {
#IBOutlet weak var TileGrid: WKInterfaceTable!
let numRows = 3
let imageNames = ["img1","img2","img3"]
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
initBoard()
}
func initBoard() {
self.TileGrid.setNumberOfRows(self.numRows, withRowType: "Row")
for var i = 0; i < numRows; i++ {
let tmpImageA = UIImage(named: imageNames[0])
let tmpImageB = UIImage(named: imageNames[1])
let rowOfTiles = self.TileGrid.rowControllerAtIndex(i) as Row
rowOfTiles.backImageA = tmpImageA
rowOfTiles.backImageB = tmpImageB
}
}
func doSomething() {
//this function needs to be called whenever a button is pressed
}
}
///////MY ROW CONTROLLER CLASS ////////////////////////
class Row : NSObject {
// At runtime these will be assigned based on the random game board creation
var backImageA = UIImage(named: "default.png")
var backImageB = UIImage(named: "default.png")
#IBOutlet weak var TileA: WKInterfaceButton!
#IBOutlet weak var TileB: WKInterfaceButton!
#IBAction func TapTileA() {
TileA.setBackgroundImage(backImageA)
//NEED TO CALL FUNCTION DO SOMETHING IN INTERFACE CONTROLLER
}
#IBAction func TapTileB() {
TileB.setBackgroundImage(backImageB)
//NEED TO CALL FUNCTION DO SOMETHING IN INTERFACE CONTROLLER
}
}
My best answer would be to create a delegate in your RowController class, then implement this delegate in your InterfaceController
The delegate could have methods buttonOneClickHappened and buttonTwoClickHappened
Then each RowController has the delegate as a property, and when you create the RowControllers call setDelegate: self where self is your InterfaceController
Hope this helps.
Related
I'm a relatively new developer and have used protocols/delegates fairly regularly but I am stuck on getting this one to implement properly. I have a view within my view controller that is assigned a class for a StarRatingView. I need to capture within the viewcontroller class the float value that the StarRatingView generates.
The star rating is working fine. I'm just not able to set the delegate variable for some reason.
here's where I define the protocol and the delegate variable.
protocol StarRatingProtocol {
func receiveStarRating(_ touchedStarRating: Float)
}
#IBDesignable
class StarRatingView: UIView {
var delegate: StarRatingProtocol? //THIS IS ALWAYS NIL NOT GETTING SET
Here's where I call the delegate function
fileprivate func touched(touch: UITouch, moveTouch: Bool) {
guard !moveTouch || lastTouch == nil || lastTouch!.timeIntervalSince(Date()) < -0.1 else { return }
print("processing touch")
guard let hs = self.hstack else { return }
let touchX = touch.location(in: hs).x
let ratingFromTouch = 5*touchX/hs.frame.width
var roundedRatingFromTouch: Float!
switch starRounding {
case .roundToHalfStar, .ceilToHalfStar, .floorToHalfStar:
roundedRatingFromTouch = Float(round(2*ratingFromTouch)/2)
case .roundToFullStar, .ceilToFullStar, .floorToFullStar:
roundedRatingFromTouch = Float(round(ratingFromTouch))
}
self.rating = roundedRatingFromTouch
lastTouch = Date()
guard delegate != nil else {return}
delegate!.receiveStarRating(roundedRatingFromTouch)
}
This is my view controller class where I set the protocol and try to set the delegate value . Should I be setting the delegate value somewhere other than viewdidload?
class LogController: UIViewController, StarRatingProtocol {
#IBOutlet weak var starRatingView: StarRatingView!
#IBOutlet weak var labelStarRating: UILabel!
let starRating = StarRatingView()
var testRating: Float?
override func viewDidLoad() {
super.viewDidLoad()
starRating.delegate = self
}
The problem is that when you say let starRating = StarRatingView(), that is a new and different StarRatingView that you are creating right now. That's not what you want; you want the StarRatingView that already exists in the interface. Presumably, if the outlet is hooked up properly, that would be self.starRatingView. That is the StarRatingView whose delegate you need to set.
Another (unrelated) problem is that the delegate needs to be weak. Otherwise there will be a retain cycle and a memory leak.
Because I needed a UINavigationController inside another UINavigationController (and this is not possible by default), I created a UIViewController that acts as a UINavigationController, but dit does not subclass from UINavigationController.
The second NavigationController (the one that does to subclass UINavigationController), presents (depending on the ViewModel's state) a controller.
This is the custom NavigationController:
class OnboardingViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var containerView: UIView!
// MARK: - Internal
private var viewModel: OnboardingViewModel = OnboardingViewModel()
// MARK: - View flow
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = true
navigateToNextFlow()
}
override var preferredFocusEnvironments: [UIFocusEnvironment] {
switch viewModel.state {
case .recommendations: return recommendationsController.preferredFocusEnvironments
default: return super.preferredFocusEnvironments
}
}
// MARK: - Handlers
var navigateToNextHandler: (() -> Void)?
// MARK: - Controllers
private var recommendationsController: OnboardingRecommendationsViewController {
let controller = UIViewController.instantiate(from: "Onboarding Recommendations") as OnboardingRecommendationsViewController
controller.navigateToNextHandler = { [unowned self] in
self.viewModel.state = .done
self.navigateToNextFlow(animated: true)
}
return controller
}
// MARK: - Navigation
private func navigateToNextFlow(animated: Bool = false) {
switch viewModel.state {
case .recommendations:
add(child: recommendationsController, to: containerView)
case .done:
viewModel.finish()
navigateToNextHandler?()
}
updateFocusIfNeeded()
setNeedsFocusUpdate()
}
}
This is the childViewController:
class OnboardingRecommendationsViewController: UIViewController {
// MARK: - Outlets
#IBOutlet weak var onOffButton: UIButton!
#IBOutlet weak var finishButton: UIButton!
// MARK: - Internal
fileprivate let viewModel: OnboardingRecommendationsViewModel = OnboardingRecommendationsViewModel()
// MARK: - View flow
override func viewDidLoad() {
super.viewDidLoad()
setupLabels()
setupOnOffButton()
}
// MARK: - Handlers
var navigateToNextHandler: (() -> Void)?
// MARK: - Focus
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [finishButton, onOffButton]
}
}
The finishButton is beneath the onOffButton in the storyboard. I'm trying to set the initial focus on the finishButton instead of the onOffButton. But the user can focus the onOffButton if he wants.
Whatever I try, it just doesn't work. The preferredFocusEnvironments gets called, but the focus of the buttons stays in the wrong order.
What am I doing wrong?
Sorry for the late answer. It turned out that the viewController I was pushing, defined was as let, so I pushed a few instances over themselves, and that's why it seems that the preferredFocusEnvironments was not working. I actually saw a new instance of the ViewController with another initial focus order. Changing the variable declaration of the viewController from let to lazy var did the trick. So, in the end, it had really nothing to do with preferredFocusEnvironments not working. But thanks for the input!
Did you try like this?
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [finishButton]
}
Or you can disable userInteraction for onOffButton until finishButton gets focused. (Not a good solution though)
You should set restoresFocusAfterTransition = false to avoid the default behavior. And then, in preferredFocusEnvironments return the view you want to focus
I have a delegation/initialization problem I can't seem to solve. Basically I have a storyboard with a few View controllers. Inside the storyboard there is this "View controller" which consists of a UITableview that I have connected with a DeviceListViewController class so that it populates the information. In here I have declared the following protocol:
protocol DeviceListViewControllerDelegate: UIAlertViewDelegate {
var connectionMode:ConnectionMode { get }
func connectPeripheral(peripheral:CBPeripheral, mode:ConnectionMode)
func stopScan()
func startScan()
}
and inside the class itself I have a init method like this (which is probably wrong but I didn't know what else I could do at this point):
convenience init(aDelegate: DeviceListViewControllerDelegate) {
self.init()
self.delegate = aDelegate
}
Then there is this second class that is not attached to any view controller called BLEMainViewController. It should be a singleton handling all the bluetooth actions. This means I should be able to delegate some stuff between DevicelistViewController and BLEMainViewController.
In the BLEMainViewController I have inherited the DeviceListViewControllerDelegate:
class BLEMainViewController: NSObject, DeviceListViewControllerDelegate {
var deviceListViewController:DeviceListViewController!
var delegate: BLEMainViewControllerDelegate?
static let sharedInstance = BLEMainViewController()
}
override init() {
super.init()
// deviceListViewController.delegate = self
deviceListViewController = DeviceListViewController(aDelegate: self)
}
The problem is that BLEMainViewController is not attached to any View Controller (and it shouldn't IMO) but it needs to be initialized as a singleton in order to handle all the BLE actions. Can anyone point me in the right direction (with an example preferably) on how to work around this?
I think you simply used wrong code architecture.
The BLEManager is a shared-instance, you can call it from everywhere, set it properties, and call its methods.
Its can delegate your view-controller with any predefine events you will add to its protocol and provide proper implementation
Here is some code, hope it helps
protocol BLEManagerDelegate{
func bleManagerDidStartScan(manager : BLEManager)
}
class BLEManager: NSObject {
static let sharedInstance = BLEManager()
var delegate: BLEManagerDelegate?
var devices : [AnyObject] = []
func startScan(){
delegate?.bleManagerDidStartScan(self)
//do what ever
}
func stopScan(){
}
}
In my app I have one screen divided between two ViewControllers - LadderViewController and GameHistoryTableViewController, which lies in a container. I want user to be able to filter the data in the table by tapping on something in the LadderView. I tried to solve this using delegates:
LadderViewController:
delegate = GameHistoryTableViewController()
func imageTapped(imageIndex: Int) {
delegate?.selectedHeroNumber(imageIndex)
}
GameHistoryTableViewController: (conforms to the delegate protocol and implemets a function from it)
func selectedHeroNumber(heroNumber: Int) {
let filteredGames = filterGamesFromHeroNumber(heroNumber)
tableDataSource = filteredGames
self.tableView.reloadData()
}
That doesn't work, though, because the delegate I declare in LadderViewConroller is another instance of GameHistoryTableViewController, not the (to the user) shown one. I don't know how to access the "visible" instance (table) of GameHistoryTableViewController though... So, how should be delegating used here? Or should I use another approach (and if so, what kind)? I basically need to change the table's data source according to on what the user taps, one can say "from outside" (dataSource is a property in my GameHistoryTableViewController class).
Here is an example with delegation like you want to do. It's a better solution than singleton in this case ;)
declare a new protocol call HeroInfo:
protocol HeroInfo: class {
func selectedHeroNumber(heroNumber: Int);
}
LadderViewController:
//create the delegation
weak var delegate:HeroInfo?
func imageTapped(imageIndex: Int) {
//call the delegate method
delegate?.selectedHeroNumber(imageIndex)
}
GameHistoryTableViewController:
// Here get the protocol HeroInfo inheritance
class userTableViewController: UITableViewController, HeroInfo {
override func viewDidLoad() {
super.viewDidLoad()
//Here get your Ladder view in a splitView
if let split = self.splitViewController {
let controllers = split.viewControllers
self.ladderViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? ladderViewController
//register it to delegate
self.ladderViewController?.delegate = self
}
}
...
// Here is your method of your protocol that you must conform to
func selectedHeroNumber(heroNumber: Int) {
let filteredGames = filterGamesFromHeroNumber(heroNumber)
tableDataSource = filteredGames
self.tableView.reloadData()
}
...
}
There are a few ways to achieve this, I have a similar setup for which I use a model class with a singleton to store the relevant data.
For instance you could have the following
class dataModel {
static let sharedInstance = dataModel()
private var _heroNumber = Int()
private init() {}
var heroNumber: Int = {
return _heroNumber
}
func setHero(hero: Int) -> Int {
return _heroNumber
}
}
}
You can then can access this model from each of your controllers using dataModel.sharedInstance.heroNumber etc...
I have a cell which I use in many different vc because I have my app divided in different categories but all use the same cell.
Now I have a button which should trigger the event to share it via other apps like whastapp or facebook.
The problem is that depending on in which category you are you have a different view controller which will display the function.
I can make it work with one but not with 10 different vc in just one cell.
I used an extension to get the parentviewController
extension UIView {
var parentViewController: HomeViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if parentResponder is UIViewController {
return parentResponder as! HomeViewController!
}
}
return nil
}
This will obviously only work at the Home vc.
How can I work around this issue?
You can use a protocol to handle your button action in the view controller:
protocol ShareEventDelegate: class {
func didShareButtonSelected()
}
In your custom cell:
class CustomCell: UITableViewCell {
weak var shareDelegate: ShareEventDelegate?
func yourButtonAction() {
shareDelegate.didShareButtonSelected?()
}
}
Then make your ViewControllers conform to the ShareEventDelegate, for example:
extension HomeViewController: ShareEventDelegate {
func didShareButtonSelected() {
// handle your action here
}
}
And in cellForRow:
cell.shareDelegate = self