{"id":74450,"date":"2025-09-01T00:33:32","date_gmt":"2025-08-31T19:03:32","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=74450"},"modified":"2025-09-17T11:47:25","modified_gmt":"2025-09-17T06:17:25","slug":"how-to-control-focus-in-swiftui-for-apple-tv-apps","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/how-to-control-focus-in-swiftui-for-apple-tv-apps\/","title":{"rendered":"A Guide to Handling Focus in Apple TV Apps with SwiftUI"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>In Apple TV, the centre of user interaction is the focus engine. Apple TV does not use the touchscreen like iPhones and iPads, where one taps everything directly on the screen, but uses the remote control to navigate among the elements. This is to say that the usability of your app largely relies on the ease and predictability with which the focus between one view and another transitions. Without good management of focus, users can become lost, frustrated or stuck on a screen. However, when done right, focus management causes your application to feel natural, responsive, and pleasant to use.<\/p>\n<p>SwiftUI has now gained more focus management capabilities, such as focusable, focus state, and custom focus guides, as well as better navigation control. Be it a settings screen, a media carousel, or a content grid, focus behavior is a key aspect to master to create a smooth and engaging user experience on tvOS.<\/p>\n<p>In this blog, we\u2019ll explore:<\/p>\n<ul style=\"list-style-type: disc;\">\n<li>What is the focus engine and how does it work.<\/li>\n<li>Learning to make elements focusable in SwiftUI.<\/li>\n<li>Managing programmatic focus with @FocusState<\/li>\n<li>Handling complex layouts like carousels and grids.<\/li>\n<\/ul>\n<p>By the end, you will know how to create hassle-free and user-friendly Apple TV apps that users will touch and feel as they navigate with the remote controller.<\/p>\n<p>Let&#8217;s begin with fundamentals:<\/p>\n<h3><strong>1. Enabling an element to be focusable:<\/strong><\/h3>\n<p>You can make a view focusable in SwiftUI for tvOS using the focusable() modifier. Here&#8217;s an easy example to know how it works.<\/p>\n<div id=\"attachment_74397\" style=\"width: 635px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM.png\"><img aria-describedby=\"caption-attachment-74397\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74397 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM-1024x276.png\" alt=\"focusableImage\" width=\"625\" height=\"168\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM-1024x276.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM-300x81.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM-768x207.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM-624x168.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-11.44.54\u202fAM.png 1208w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><\/a><p id=\"caption-attachment-74397\" class=\"wp-caption-text\">focusableImage<\/p><\/div>\n<h3><strong>2. Switch focus between two buttons:<\/strong><\/h3>\n<p>To control our focus between buttons, we bought to remember which button we are focused on and update the UI to indicate the same. This is implemented with SwiftUI with the FocusState.<\/p>\n<p><strong> Steps to handle focus:<\/strong><\/p>\n<ol style=\"list-style-type: lower-alpha;\">\n<li><strong>Define focusable field<\/strong>: Create an enum <strong>FocusField<\/strong> (e.g., .button1, .button2) to represent the focusable buttons.<\/li>\n<li><strong>Track the focused element:<\/strong> Use @<strong>FocusState<\/strong> var focusedField: FocusField? to keep track of the currently focused button.<\/li>\n<li><strong>Change focus of buttons:<\/strong> \u00a0use .<strong>focused<\/strong>($focusedField, equals: .button1).<\/li>\n<li><strong>Update UI based on focus:<\/strong> Yellow if focused and Gray if not focused.<\/li>\n<li><strong>Set initial focus<\/strong>: In .<strong>onAppear<\/strong>, set the default focus.<\/li>\n<\/ol>\n<p>A simple example to demonstrate how it works<\/p>\n<div id=\"attachment_76155\" style=\"width: 635px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM.png\"><img aria-describedby=\"caption-attachment-76155\" decoding=\"async\" loading=\"lazy\" class=\"size-large wp-image-76155\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-1024x185.png\" alt=\"defaultFocusbutton\" width=\"625\" height=\"113\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-1024x185.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-300x54.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-768x139.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-1536x277.png 1536w, \/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM-624x113.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/09\/Screenshot-2025-09-09-at-3.47.41\u202fPM.png 1840w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><\/a><p id=\"caption-attachment-76155\" class=\"wp-caption-text\">defaultFocusbutton<\/p><\/div>\n<div id=\"attachment_74406\" style=\"width: 635px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM.png\"><img aria-describedby=\"caption-attachment-74406\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74406 size-large\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM-1024x839.png\" alt=\"focusButton\" width=\"625\" height=\"512\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM-1024x839.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM-300x246.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM-768x629.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM-624x511.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-12.39.24\u202fPM.png 1484w\" sizes=\"(max-width: 625px) 100vw, 625px\" \/><\/a><p id=\"caption-attachment-74406\" class=\"wp-caption-text\">focusButton<\/p><\/div>\n<p>By using <strong>.focused<\/strong>($focusedField, equals: .button1), we are able to set and follow the current focus. To dynamically change the color of text when focused, we check the current focusedField value and set an alternate color when the element has focus.<\/p>\n<h3><strong>3. Manually Moving Focus to a Specific Element When Alignment Breaks:<\/strong><\/h3>\n<p>In SwiftUI for tvOS, when handling this scenario, we can use <strong>onMoveCommand<\/strong> to implement custom focus navigation. Let\u2019s look at some sample code to see how we can move focus between elements.<\/p>\n<ul style=\"list-style-type: disc;\">\n<li>As we have <strong>pressesBegan<\/strong> in UIKit, similarly in SwiftUI, we have <strong>&#8220;customFocusNavigation&#8221;<\/strong> to get the direction of the remote from the current focus element.<\/li>\n<li>So, according to the direction we get from the <strong>onMoveCommand<\/strong>, we can set our custom focus.<\/li>\n<\/ul>\n<p>A simple example to demonstrate how it works<\/p>\n<div id=\"attachment_74425\" style=\"width: 310px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM.png\"><img aria-describedby=\"caption-attachment-74425\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74425 size-medium\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM-300x154.png\" alt=\"un-alignButton\" width=\"300\" height=\"154\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM-300x154.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM-1024x525.png 1024w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM-768x394.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM-624x320.png 624w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.09.42\u202fPM.png 1076w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><p id=\"caption-attachment-74425\" class=\"wp-caption-text\">un-alignButton<\/p><\/div>\n<p>The above image shows two buttons placed unevenly on the left and right sides of the screen. In such cases, the default focus engine may not provide smooth navigation between them.<\/p>\n<div id=\"attachment_74427\" style=\"width: 801px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM.png\"><img aria-describedby=\"caption-attachment-74427\" decoding=\"async\" loading=\"lazy\" class=\"wp-image-74427 \" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM.png\" alt=\"customFocusNavigation\" width=\"791\" height=\"280\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM.png 972w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM-300x106.png 300w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM-768x272.png 768w, \/blog\/wp-ttn-blog\/uploads\/2025\/08\/Screenshot-2025-08-28-at-3.11.59\u202fPM-624x221.png 624w\" sizes=\"(max-width: 791px) 100vw, 791px\" \/><\/a><p id=\"caption-attachment-74427\" class=\"wp-caption-text\">customFocusNavigation<\/p><\/div>\n<ul style=\"list-style-type: disc;\">\n<li>\u00a0<strong>OnMoveCommand<\/strong> assists us in creating personalized up\/down navigation such that when we press the arrows of the remote, we travel focus according to our preference.<\/li>\n<li>\u00a0In such a way, although the alignment of UI elements might not match, we can still control the flow of focus manually and offer a smooth experience of the navigation.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>The essence of a great tvOS is focus management. Although the default Focus Engine is sufficient to handle most scenarios, asynchronously placed elements may have to be handled with specialized control. Using both onMoveCommand and @FocusState, we can take control of the flow of navigation such that a user is always returned to the right element with an animative flow that is both seamless and predictable. But not only is it more usable, but it also gives us a good interface to the apps on the tvOS.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction In Apple TV, the centre of user interaction is the focus engine. Apple TV does not use the touchscreen like iPhones and iPads, where one taps everything directly on the screen, but uses the remote control to navigate among the elements. This is to say that the usability of your app largely relies on [&hellip;]<\/p>\n","protected":false},"author":2138,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":431},"categories":[3477],"tags":[5484,7892,3189,2715,5460,3531],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74450"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2138"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=74450"}],"version-history":[{"count":13,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74450\/revisions"}],"predecessor-version":[{"id":76513,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/74450\/revisions\/76513"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=74450"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=74450"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=74450"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}