{"id":9732,"date":"2026-07-31T13:45:00","date_gmt":"2026-07-31T10:45:00","guid":{"rendered":"https:\/\/handoli.com\/index.php\/2026\/07\/31\/swift-protocols-and-the-main-actor\/"},"modified":"2026-07-31T13:45:00","modified_gmt":"2026-07-31T10:45:00","slug":"swift-protocols-and-the-main-actor","status":"publish","type":"post","link":"https:\/\/handoli.com\/index.php\/2026\/07\/31\/swift-protocols-and-the-main-actor\/","title":{"rendered":"Swift protocols and the main actor"},"content":{"rendered":"<p>In Swift, the <code>@MainActor<\/code> attribute can be used to associate a given type, function, or value with the global <em>main actor<\/em>, which essentially makes any such code operate on the enclosing app\u2019s main thread. That\u2019s incredibly useful for things like UI-related code, since UI updates (whether done using UIKit, SwiftUI, or another framework) most often need to be performed on the main thread. But how does all of this work when it comes to declaring and adopting <em>protocols<\/em>?<\/p>\n<h2>The main actor as a requirement<\/h2>\n<p>Just like concrete types, a Swift protocol can be fully isolated to the main actor, which means that all of its members (which in the case of a protocol are its list of requirements, and any extensions we\u2019ve added) in turn also become main actor isolated. For example, here we\u2019ve declared an <code>ErrorPresenter<\/code> protocol, which is annotated with the <code>@MainActor<\/code> attribute:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">@MainActor<\/span> <span class=\"s-keyword\">protocol<\/span> ErrorPresenter {\n    <span class=\"s-keyword\">func<\/span> presentError(<span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>, fromView view: <span class=\"s-type\">UIView<\/span>)\n    \n    <span class=\"s-keyword\">func<\/span> presentRecoverableError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>,\n        onRetry: <span class=\"s-keyword\">@escaping<\/span> () -&gt; <span class=\"s-type\">Void<\/span>\n    )\n}<\/code><\/pre>\n<p>Isolating the above two function requirements to the main actor makes a lot of sense, since they are using UIKit\u2019s <code>UIView<\/code> for error presentation, which in turn is a main thread-only type. However, by marking our entire <code>ErrorPresenter<\/code> protocol as <code>@MainActor<\/code>, we\u2019ve actually done more than just isolate those two functions \u2014 we\u2019ve made main actor isolation a <em>requirement<\/em> for any types conforming to our protocol, which can sometimes be quite unexpected.<\/p>\n<p>Take the following <code>Navigator<\/code> class, for example, which is currently non-isolated (unless the enclosing project\u2019s <code>Default Actor Isolation<\/code> build setting has been set to <code>MainActor<\/code>):<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">final class<\/span> Navigator: <span class=\"s-type\">Sendable<\/span> {\n    ...\n}<\/code><\/pre>\n<p>Now, if we simply make the above class conform to our <code>ErrorPresenter<\/code> protocol, then we\u2019re actually also changing the actor isolation of that entire class in the process, which we wouldn\u2019t be able to tell unless we already know that our protocol is <code>@MainActor<\/code>-marked:<\/p>\n<pre class=\"splash\"><code><span class=\"s-comment\">\/\/ This class is now main actor isolated, even though we haven't\n\/\/ added the @MainActor annotation explicitly:<\/span>\n<span class=\"s-keyword\">final class<\/span> Navigator: <span class=\"s-type\">Sendable<\/span>, <span class=\"s-type\">ErrorPresenter<\/span> {\n    ...\n}<\/code><\/pre>\n<p>However, if we instead conform to <code>ErrorPresenter<\/code> using an extension, then our <code>Navigator<\/code> class remains non-isolated, and only the protocol requirements implemented within that extension becomes isolated to the main actor:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">Navigator<\/span>: <span class=\"s-type\">ErrorPresenter<\/span> {\n    <span class=\"s-keyword\">func<\/span> presentError(<span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>, fromView view: <span class=\"s-type\">UIView<\/span>) {\n        ...\n    }\n\n    <span class=\"s-keyword\">func<\/span> presentRecoverableError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>,\n        onRetry: <span class=\"s-keyword\">@escaping<\/span> () -&gt; <span class=\"s-type\">Void<\/span>\n    ) {\n        ...\n    }\n}<\/code><\/pre>\n<p>Although the above behaviors might be unexpected, they don\u2019t tend to cause that many problems in practice \u2014 both given that some projects now use <code>MainActor<\/code> as their default isolation, and given that we can always prevent any conforming types from becoming main actor isolated using an extension, like we did above.<\/p>\n<p>But what if we\u2019re looking to make an actual <code>actor<\/code> type conform to our <code>ErrorPresenter<\/code> protocol? For example like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">actor<\/span> ExportService {\n    ...\n}\n\n<span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">ExportService<\/span>: <span class=\"s-type\">ErrorPresenter<\/span> {\n    ...\n}<\/code><\/pre>\n<p>At first glance, the above <em>should<\/em> work, given that we\u2019re following the exact same pattern as when making our non-isolated class adopt <code>ErrorPresenter<\/code>. However, in this case, we actually get a compiler error:<\/p>\n<pre><code class=\"no-highlight\">Actor 'ExportService' cannot conform to global-actor-isolated\nprotocol 'ErrorPresenter'<\/code><\/pre>\n<p>So is there any way that we can make a protocol that declares main actor-only requirements compatible with custom actors that we\u2019ve defined ourselves?<\/p>\n<h2>Marking individual members instead<\/h2>\n<p>An alternative to making an entire protocol main actor-bound is to instead mark individual requirements as <code>@MainActor<\/code>. That way, we\u2019ve essentially decoupled the protocol itself from that isolation, which gives us a much more flexible setup (even though it does require us to repeat that <code>@MainActor<\/code> annotation multiple times). Here\u2019s what that would look like for our <code>ErrorPresenter<\/code> protocol:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">protocol<\/span> ErrorPresenter {\n    <span class=\"s-keyword\">@MainActor<\/span> <span class=\"s-keyword\">func<\/span> presentError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>\n    )\n\n    <span class=\"s-keyword\">@MainActor<\/span> <span class=\"s-keyword\">func<\/span> presentRecoverableError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>,\n        onRetry: <span class=\"s-keyword\">@escaping<\/span> () -&gt; <span class=\"s-type\">Void<\/span>\n    )\n}<\/code><\/pre>\n<p>With the above changes in place, we can now make our <code>ExportService<\/code> actor from before adopt our protocol without any problems:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">extension<\/span> <span class=\"s-type\">ExportService<\/span>: <span class=\"s-type\">ErrorPresenter<\/span> {\n    <span class=\"s-keyword\">@MainActor func<\/span> presentError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>\n    ) {\n        ...\n    }\n\n    <span class=\"s-keyword\">@MainActor func<\/span> presentRecoverableError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>,\n        onRetry: <span class=\"s-keyword\">@escaping<\/span> () -&gt; <span class=\"s-type\">Void<\/span>\n    ) {\n        ...\n    }\n}<\/code><\/pre>\n<p>As intended, the <code>presentError<\/code> and <code>presentRecoverableError<\/code> functions are still main actor-isolated, but they can now be freely mixed with both non-isolated code, as well as code isolated to any custom actor that we\u2019ve declared, like the one used above.<\/p>\n<p>An additional benefit to taking this more granular <code>@MainActor<\/code> approach when it comes to protocols is that it also lets us mix isolated and non-isolated requirements within the same protocol. For example, let\u2019s say that we wanted to add a function to our <code>ErrorPresenter<\/code> protocol that lets a conforming type opt out of presenting a given error. Since that doesn\u2019t involve any UI at all there\u2019s no need to make such a function main actor isolated, which can now be done by simply omitting that attribute from that function declaration \u2014 like this:<\/p>\n<pre class=\"splash\"><code><span class=\"s-keyword\">protocol<\/span> ErrorPresenter {\n    <span class=\"s-keyword\">func<\/span> shouldPresentError(<span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>) -&gt; <span class=\"s-type\">Bool<\/span>\n\n    <span class=\"s-keyword\">@MainActor func<\/span> presentError(<span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>, fromView view: <span class=\"s-type\">UIView<\/span>)\n\n    <span class=\"s-keyword\">@MainActor func<\/span> presentRecoverableError(\n        <span class=\"s-keyword\">_<\/span> error: <span class=\"s-type\">Error<\/span>,\n        fromView view: <span class=\"s-type\">UIView<\/span>,\n        onRetry: <span class=\"s-keyword\">@escaping<\/span> () -&gt; <span class=\"s-type\">Void<\/span>\n    )\n}<\/code><\/pre>\n<p>Note that when implementing the above new <code>shouldPresentError<\/code> function within our <code>ExportService<\/code> actor, we need to explicitly mark it as <code>nonisolated<\/code> (since that\u2019s what it is), which in turn prevents us from accessing the actor\u2019s own state (since that\u2019s isolated to the actor). One way that we can fix that would be to instead mark <code>shouldPresentError<\/code> as <code>async<\/code>, which makes it fully compatible with any kind of actor isolation.<\/p>\n<h2>Conclusion<\/h2>\n<p>So does that mean that we should <em>never<\/em> fully associate any protocol with the main actor, and instead only annotate individual requirements with <code>@MainActor<\/code>? My personal approach is to avoid isolating entire protocols unless they are clearly only ever going to be adopted by main actor bound types. For example, any protocol that requires the implementing type to be a SwiftUI <code>View<\/code> or UIKit <code>UIView\/UIViewController<\/code>, or that\u2019s related to main thread-only rendering, and so on.<\/p>\n<p>In most other cases, I do prefer marking individual requirements, just like we did in this article, since that results in the maximum amount of flexibility, and lets me remain selective about where I deploy the <code>@MainActor<\/code> attribute \u2014 since especially when adopting Swift 6 and strict concurrency, it can be quite easy to fall into the <em>\u201cmain actor all the things\u201d<\/em> trap. But more on that in future articles.<\/p>\n<p>Hope you found this article useful, and thanks for reading!<\/p>","protected":false},"excerpt":{"rendered":"<p>In Swift, the @MainActor attribute can be used to associate a given type, function, or value with the global main actor, which essentially makes any such code operate on the enclosing app\u2019s main thread. That\u2019s incredibly useful for things like UI-related code, since UI updates (whether done using UIKit, SwiftUI, or another framework) most often [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rop_custom_images_group":[],"rop_custom_messages_group":[],"rop_publish_now":"initial","rop_publish_now_accounts":[],"rop_publish_now_history":[],"rop_publish_now_status":"pending","footnotes":""},"categories":[1,15],"tags":[],"class_list":["post-9732","post","type-post","status-publish","format-standard","hentry","category-explore","category-world"],"_links":{"self":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9732","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/comments?post=9732"}],"version-history":[{"count":0,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/posts\/9732\/revisions"}],"wp:attachment":[{"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/media?parent=9732"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/categories?post=9732"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/handoli.com\/index.php\/wp-json\/wp\/v2\/tags?post=9732"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}