I’m engaged on an iOS app utilizing UIKit and Core Information that entails graph-based navigation the place nodes characterize completely different strikes in a martial arts system. Customers can navigate by way of these nodes in numerous modes, together with Particular Sparring Mode, which introduces randomness in node choice.
The whole lot works positive till a Fail Node seems. At that time:
A standard node begins incorrectly redirecting to a Fail Node’s Major as an alternative of its supposed connections.
This situation persists till the person refreshes the web page.
Fail Nodes themselves by no means redirect incorrectly, which means solely regular nodes get corrupted.
Anticipated Conduct –
Solely Secondary Nodes ought to redirect to their corresponding Major Nodes.
Fail Nodes mustn’t intervene with the navigation logic of Regular Nodes.
Debugging navigateToPrimaryNode(for:)
I added print() statements to trace which nodes had been being redirected.
Discovered that after encountering a Fail Node, a standard node unexpectedly began redirecting to a Fail Node’s Major.
Checking startSpecificSparring(for:)
I logged the nodes that had been being hidden or proven.
Verified that Fail Nodes had been being displayed appropriately, however some regular nodes had been disappearing or redirecting incorrectly.
Investigating updateLinePosition(for:) and redrawLines()
I checked whether or not incorrect traces had been being drawn between nodes after encountering a Fail Node.
Discovered that the visible illustration of the nodes appeared positive, which means the difficulty wasn’t purely a UI bug.
Resetting & Retrying Particular Sparring Mode
I used retrySpecificSparring() to see if the difficulty resolved itself on a retry.
The problem persevered even after retrying, which means one thing was being modified persistently.
Verifying restoreTextFields()
Restarted the app to see if Core Information was saving incorrect relationships.
Confirmed that the difficulty disappears after refreshing, which means it’s probably not a Core Information storage situation, however one thing taking place dynamically throughout navigation.
What I Anticipated
Secondary nodes ought to solely redirect to their respective Major nodes.
Fail Nodes mustn’t intervene with regular nodes.
After a failure, regular nodes ought to proceed functioning as anticipated.
Retrying Particular Sparring ought to reset node conduct correctly.
Findings –
Regular nodes typically get “caught” redirecting to a Fail Node’s Major.
The problem solely occurs after encountering a Fail Node.
Refreshing the app fixes the difficulty, which means it isn’t being saved incorrectly in Core Information.
The issue persists throughout retries however disappears on full web page refresh.
That is all of my relative code items.
func navigateToPrimaryNode(for node: DragableNode) -> DragableNode {
// ✅ If node is already major, return it instantly
if node.sort == .major {
return node
}
// ✅ If node is secondary, discover its appropriate major node
if node.sort == .secondary, let primaryNode = findPrimaryNode(for: node) {
print("🔄 Navigating Secondary (node.textual content ?? "Unnamed") to Major (primaryNode.textual content ?? "Unnamed")")
return primaryNode
}
// ✅ If it is a fail node, verify if it has a major fail node
if node.entity.nodeType == .fail, let primaryFailNode = findPrimaryNode(for: node) {
print("⚠️ Redirecting Fail Node (node.textual content ?? "Unnamed") to Major (primaryFailNode.textual content ?? "Unnamed")")
return primaryFailNode
}
// 🔴 Particular Case: Stop fail nodes from overriding regular node redirects
if node.entity.nodeType != .fail {
if let cachedPrimary = findPrimaryNode(for: node) {
print("✅ Making certain regular node (node.textual content ?? "Unnamed") stays mapped to its precise major (cachedPrimary.textual content ?? "Unnamed")")
return cachedPrimary
}
}
print("⚠️ No major discovered for (node.textual content ?? "Unnamed"), returning authentic node")
return node
}
Piece 2
func findPrimaryNode(for node: DragableNode) -> DragableNode? {
guard node.sort == .secondary else { return nil }
let primaryCandidates = textFields.filter {
$0.sort == .major && $0.textual content == node.textual content
}
if primaryCandidates.isEmpty {
print("⚠️ No major node discovered for secondary node: (node.textual content ?? "Unnamed")")
return nil
}
if primaryCandidates.rely == 1 {
print("✅ Discovered single major node: (primaryCandidates.first!.textual content ?? "Unnamed")")
return primaryCandidates.first
}
// 🔥 If a number of major nodes exist, use the entity relationship as a backup verify
for major in primaryCandidates {
if major.entity.kids?.accommodates(node.entity) == true {
print("✅ Chosen appropriate major node: (major.textual content ?? "Unnamed") for secondary node: (node.textual content ?? "Unnamed")")
return major
}
}
print("⚠️ A number of matching major nodes discovered, defaulting to first: (primaryCandidates.first!.textual content ?? "Unnamed")")
return primaryCandidates.first
}
piece 3
// Helper operate to search out the first textual content area with a selected identify
func findPrimaryTextField(named identify: String) -> DragableNode? {
return textFields.first { $0.textual content == identify && $0.sort == .major }
}
piece 4
@objc func handleNodeTap(_ sender: UITapGestureRecognizer) {
guard let tappedNode = sender.view as? DragableNode else { return }
// ✅ If tapped node is a Secondary Node, redirect to the Major Node
if tappedNode.sort == .secondary, let primaryNode = findPrimaryNode(for: tappedNode) {
print("🔄 Redirecting from Secondary (tappedNode.textual content ?? "Unnamed") to Major (primaryNode.textual content ?? "Unnamed")")
enterViewMode(for: primaryNode)
return
}
if isInViewMode {
enterViewMode(for: tappedNode)
return
}
if isInSpecificSparring {
print("🔥 Particular Sparring: Transferring to Subsequent Stage from (tappedNode.textual content ?? "Unnamed Node")")
startSpecificSparring(for: tappedNode, problem: currentSparringDifficulty)
// ✅ Recenter and zoom to suit new nodes after choosing one
DispatchQueue.foremost.asyncAfter(deadline: .now() + 0.1) {
let visibleNodes = self.contentView.subviews.compactMap { $0 as? DragableNode }.filter { !$0.isHidden }
self.recenterAndZoomToFit(nodes: visibleNodes)
}
return
}
// Animate the present node scaling up
UIView.animate(withDuration: 0.3, animations: {
tappedNode.rework = CGAffineTransform(scaleX: 1.2, y: 1.2)
tappedNode.alpha = 1.0
}, completion: { _ in
// Reset the scaling again to regular
UIView.animate(withDuration: 0.2) {
tappedNode.rework = CGAffineTransform.id
}
// 🔥 Transition to the brand new node
self.enterViewMode(for: tappedNode)
})
}
piece 5
@objc non-public func retrySpecificSparring() {
guard let firstNode = firstViewModeNode else {
print("⚠️ No firstViewModeNode discovered! Restarting from the final used node.")
if let lastUsedNode = textFields.first(the place: { !$0.isHidden }) {
firstViewModeNode = lastUsedNode
startSpecificSparring(for: lastUsedNode, problem: currentSparringDifficulty)
}
return
}
print("🔄 Retrying Particular Sparring from (firstNode.textual content ?? "Unnamed")")
// ✅ Resetting the View
DispatchQueue.foremost.async {
for subview in self.contentView.subviews {
if let textNode = subview as? DragableNode {
textNode.isHidden = true
textNode.alpha = 0.0
}
}
// ✅ Eradicating previous traces
self.contentView.layer.sublayers?.removeAll(the place: { $0 is CAShapeLayer })
// ✅ Restart sparring
self.startSpecificSparring(for: firstNode, problem: self.currentSparringDifficulty)
}
}
piece 6
func startSpecificSparring(for node: DragableNode, problem: Int) {
guard let superview = self.contentView else { return }
var targetNode: DragableNode = navigateToPrimaryNode(for: node)
// Debugging
print("🔥 Beginning Particular Sparring - Node: (node.textual content ?? "Unnamed") (Sort: (node.sort))")
print("🔄 Redirecting to Major Node: (targetNode.textual content ?? "Unnamed") (Sort: (targetNode.sort))")
if firstViewModeNode == nil {
firstViewModeNode = targetNode
print("✅ Setting firstViewModeNode to (targetNode.textual content ?? "Unnamed Node")")
}
// ✅ Guarantee the proper node is seen and up to date instantly
DispatchQueue.foremost.async {
targetNode.isHidden = false
targetNode.alpha = 1.0
self.contentView.bringSubviewToFront(targetNode)
}
// 🔥 Guarantee major nodes are delivered to the entrance to keep away from contact interference
for subview in contentView.subviews {
if let primaryNode = subview as? DragableNode, primaryNode.sort == .major {
contentView.bringSubviewToFront(primaryNode)
}
}
isInSpecificSparring = true
currentSparringDifficulty = problem
// ✅ Guarantee firstViewModeNode is ready when coming into sparring mode
if firstViewModeNode == nil {
firstViewModeNode = targetNode
}
// ✅ Roll a random quantity (1-10)
let winChance = Int.random(in: 1...10)
// ✅ Decide successful/shedding numbers primarily based on problem
let (winningNumbers, losingNumbers) = getWinningLosingNumbers(for: problem)
let didWin = winningNumbers.accommodates(winChance)
print("🎲 Random Roll: (winChance) → (didWin ? "SUCCESS" : "FAIL")")
// ✅ Cover all nodes initially
for subview in superview.subviews {
if let textNode = subview as? DragableNode {
textNode.isHidden = true
}
}
// ✅ At all times preserve the chosen node seen
targetNode.isHidden = false
targetNode.alpha = 1.0
superview.bringSubviewToFront(targetNode)
// ✅ Take away all current traces
for sublayer in superview.layer.sublayers ?? [] {
if let line = sublayer as? CAShapeLayer {
line.removeFromSuperlayer()
}
}
var nodesToShow: [DragableNode] = []
// ✅ Decide which nodes ought to be seen
for subview in superview.subviews {
if let textNode = subview as? DragableNode, textNode !== targetNode {
let isConnected = targetNode.entity.kids?.accommodates(the place: { ($0 as? TextFieldsEntity) == textNode.entity }) == true
let isParent = textNode.entity.dad or mum == targetNode.entity
let isFailNode = textNode.entity.nodeType == .fail
let isAllowedSuccessType = textNode.entity.nodeType == .development ||
textNode.entity.nodeType == .transition ||
textNode.entity.nodeType == .submission
print("🟢 Checking node: (textNode.textual content ?? "Unnamed") - Sort: (textNode.entity.nodeType)")
if didWin {
// ✅ Cover all fail nodes on success
if isFailNode {
textNode.isHidden = true
print("❌ Hiding Fail Node: (textNode.textual content ?? "Unnamed") (Success Path)")
}
// ✅ Solely present success nodes
else if isConnected && isAllowedSuccessType {
print("✅ Exhibiting: (textNode.textual content ?? "Unnamed") (Success Path)")
nodesToShow.append(textNode)
}
} else {
// ✅ Solely present fail nodes
if isConnected && isFailNode {
print("✅ Exhibiting Fail Node: (textNode.textual content ?? "Unnamed") (Fail Path)")
nodesToShow.append(textNode)
}
}
}
}
// ✅ Guarantee solely the proper nodes are seen
for node in nodesToShow {
node.isHidden = false
node.alpha = 1.0
}
// ✅ Fade in linked nodes easily
fadeInConnectedNodes(nodesToShow)
// ✅ Place nodes correctly
positionNodesRelativeTo(targetNode, connectedNodes: nodesToShow)
// ✅ Auto-recenter the view to suit all seen nodes
recenterAndZoomToFit(nodes: [targetNode] + nodesToShow)
DispatchQueue.foremost.asyncAfter(deadline: .now() + 0.1) {
let visibleNodes = [targetNode] + nodesToShow
self.recenterAndZoomToFit(nodes: visibleNodes)
}
// ✅ Add Retry & Exit buttons
addRetryAndExitButtons(under: targetNode)
}
And right here is the node supervisor and dragable Node in case you want it
import Basis
import UIKit
class NodeManager {
static let shared = NodeManager()
var primaryNodes: [String: (node: DragableNode, viewController: UIViewController)] = [:]
func clearPrimaryNodes() {
print("🧹 Clearing cached major nodes")
primaryNodes.removeAll()
}
func addPrimaryNode(_ node: DragableNode, viewController: UIViewController) {
primaryNodes[node.text ?? ""] = (node, viewController)
}
func findPrimaryNode(named identify: String) -> (node: DragableNode, viewController: UIViewController)? {
return primaryNodes[name]
}
}
import UIKit
class DragableNode: UIView, UITextFieldDelegate {
var textField: UITextField
var entity: TextFieldsEntity
var traces: [CAShapeLayer] = []
non-public var panGesture: UIPanGestureRecognizer!
var isInEditMode = false
var isEditable: Bool = false {
didSet {
textField.isUserInteractionEnabled = isEditable
}
}
var identifier: UUID?
var isPrimary: Bool = false
var isSecondary: Bool = false
var textual content: String? {
get { return textField.textual content }
set { textField.textual content = newValue}
}
var attributedPlaceholder: NSAttributedString? {
get { return textField.attributedPlaceholder }
set { textField.attributedPlaceholder = newValue }
}
var sort: TextFieldType = .none {
didSet {
typeChanged(sort: sort)
}
}
// END OF STAGE
init(dimension: CGSize, entity: TextFieldsEntity) {
let body = CGRect(x: entity.xPosition, y: entity.yPosition, width: dimension.width, peak: dimension.peak)
self.textField = UITextField(body: body)
self.entity = entity
tremendous.init(body: body)
self.sort = entity.fieldType
typeChanged(sort: sort)
setupView()
setupGestures()
NotificationCenter.default.addObserver(self, selector: #selector(handleEditModeChanged(_:)), identify: .editModeChanged, object: nil)
}
init(body: CGRect, entity: TextFieldsEntity) {
self.textField = UITextField(body: body)
self.entity = entity
tremendous.init(body: body)
self.sort = entity.fieldType
typeChanged(sort: sort)
setupView()
setupGestures()
NotificationCenter.default.addObserver(self, selector: #selector(handleEditModeChanged(_:)), identify: .editModeChanged, object: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Technique not accessible")
}
override func canPerformAction(_ motion: Selector, withSender sender: Any?) -> Bool {
if motion == #selector(UIResponderStandardEditActions.selectAll(_:)) {
return false
}
return tremendous.canPerformAction(motion, withSender: sender)
}
non-public func setupView() {
textField.delegate = self
addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
// ✅ Apply default background coloration (strong coloration for textual content fields)
backgroundColor = entity.nodeType.coloration.withAlphaComponent(0.3)
// ✅ Apply node styling together with gradients, shadows, and animations
entity.nodeType.applyStyle(to: self)
layer.masksToBounds = false // Permits shadow visibility
topAnchor.constraint(equalTo: textField.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: textField.bottomAnchor).isActive = true
leadingAnchor.constraint(equalTo: textField.leadingAnchor, fixed: -8).isActive = true
trailingAnchor.constraint(equalTo: textField.trailingAnchor, fixed: 8).isActive = true
}
non-public func setupGestures() {
panGesture = UIPanGestureRecognizer(goal: self, motion: #selector(handlePan(_:)))
self.addGestureRecognizer(panGesture)
}
func startEditing() {
textField.becomeFirstResponder()
}
@objc non-public func handleEditModeChanged(_ notification: Notification) {
if let userInfo = notification.userInfo, let isInEditMode = userInfo["isInEditMode"] as? Bool {
self.isInEditMode = isInEditMode
}
}
@objc func handlePan(_ sender: UIPanGestureRecognizer) {
guard let superview = self.superview else { return }
// Calculate the interpretation
let translation = sender.translation(in: superview)
// Replace the node's heart place
self.heart = CGPoint(x: self.heart.x + translation.x, y: self.heart.y + translation.y)
sender.setTranslation(.zero, in: superview)
// Replace the entity's place within the information mannequin
entity.xPosition = self.body.origin.x
entity.yPosition = self.body.origin.y
// Notify the dad or mum view controller to deal with extra updates (e.g., traces)
var nextResponder: UIResponder? = self
whereas nextResponder != nil {
if let scrollViewController = nextResponder as? ScrollViewController {
scrollViewController.updateLinePosition(for: self)
break
}
nextResponder = nextResponder?.subsequent
}
// Deal with the gesture state for additional updates or actions
if sender.state == .ended {
// Optionally carry out any cleanup or extra updates when the gesture ends
print("✅ Pan gesture ended. Node place: (heart)")
}
}
func findViewController() -> UIViewController? {
var nextResponder: UIResponder? = self
whereas nextResponder != nil {
if let viewController = nextResponder as? UIViewController {
return viewController
}
nextResponder = nextResponder?.subsequent
}
return nil
}
non-public func typeChanged(sort: TextFieldType) {
change sort {
case .major:
self.layer.borderColor = UIColor.black.cgColor
case .secondary:
self.layer.borderColor = UIColor.white.cgColor
case .none:
self.layer.borderColor = UIColor.clear.cgColor
}
self.layer.borderWidth = 2
entity.sort = sort.rawValue
}
func textFieldDidEndEditing(_ textField: UITextField) {
entity.textual content = textField.textual content
}
}