Structuring Apps

When you create larger iced apps, you might want to have reusable components and views. For that, there is a common approach that is also used in this showcase from the founder of iced.

In this architecture a view/component is independent of the application and is split into three parts, message, the state and the action.

The state will have a normal view function that returns a iced::Element<Message>.

The update function will differ a bit. Instead of a Task like our main application, it will return an action enum. Common variants for this enum can be None, Task(iced::Task<Message>) and Submit(String). You can think of this action like a message that is sent from the view to the parent application that should handle it.

This kind of architecture will enhance composability.

Initial State

The view should also have a function that creates a new instance of that view. If some task needs to be done for the initial state of the view (i.e. fetching data), something like (Self, iced::Task<Message>) should be returned by that function. The task would be executed, and the data is sent via a message to the view.

To differ between a loaded and not loaded state you can use something like this pattern for your state:

enum State {
    NotLoaded,
    Loaded {
        field1: String,
        field2: i32
    }
}

Mapping

If you want to compose your UI of subparts that have their own functionality, it makes sense to give them their own messages.

But to use a iced::Element<ViewMessage> in your app that requires a iced::Element<AppMessage> you have to map them.

For that, you can simply use iced::Element<ViewMessage>.map(AppMessage::ViewMessage).

Note: this is a shortcut for iced::Element<ViewMessage>.map(|view_message| AppMessage::ViewMessage(view_message))

The .map function takes a closure that takes the message and converts it into another message. This often requires having a dedicated message variant on the application level that contains the view message.

Map functions like this are available on Task::map, Subscription::map, Element::map.

Example

The example shows an unfinished joke listing app with a view for creating jokes. When the user tries to create a new joke, a default joke, fetched from an API, is provided. The user can get a random joke from the API on a button click as well. All Jokes are listed in the main/default view.

This is how the view for new jokes looks like using this design:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}

As you see, the update function produces this action, that the parent of the view can handle:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}

The application that would host the view could look like this:

fn main() {
    iced::run("Project Structure Example", App::update, App::view).unwrap();
}

#[derive(Debug, Clone)]
enum Message {
    // This message is used to handle the new views message
    NewMessage(new::Message),
    New,
}

#[derive(Debug, Default)]
enum View {
    #[default]
    Default,
    Edit(new::NewView),
}

#[derive(Debug, Default)]
struct App {
    view: View,
    items: Vec<String>,
}

impl App {
    fn update(&mut self, message: Message) -> iced::Task<Message> {
        match message {
            Message::NewMessage(view_message) => {
                if let View::Edit(edit) = &mut self.view {
                    // Call the update method of the edit view 
                    // and handle the returned action
                    match edit.update(view_message) {
                        new::Action::None => {}
                        // If the action is a task, map the
                        // task to a message, the higher level message
                        new::Action::Task(task) => return task.map(Message::NewMessage),
                        new::Action::Submitted(content) => {
                            self.view = View::Default;
                            self.items.push(content);
                        }
                    }
                }
            }
            Message::New => {
                // Create a new view
                let (view, task) = new::NewView::new();
                self.view = View::Edit(view);
                // Run the task and map it to the higher level message
                return task.map(Message::NewMessage);
            }
        }
        iced::Task::none()
    }

    fn view(&self) -> iced::Element<Message> {
        match &self.view {
            View::Default => {
                let items = self
                    .items
                    .iter()
                    .map(|item| iced::widget::text(item).into())
                    .collect();
                iced::widget::column![
                    iced::widget::button("Edit").on_press(Message::New),
                    iced::widget::Column::from_vec(items)
                ]
                .into()
            }
            // If the view is an edit view, call the view method of the edit view
            // and map the returned message to the higher level message
            View::Edit(edit) => edit.view().map(Message::NewMessage),
        }
    }
}

mod new {
    pub enum Action {
        None,
        Task(iced::Task<Message>),
        Submitted(String),
    }

    #[derive(Debug, Clone)]
    pub enum Message {
        Submit,
        SetChanged(String),
        RandomJoke,
    }

    #[derive(Debug, Default)]
    pub struct NewView {
        content: String,
    }

    impl NewView {
        pub fn new() -> (Self, iced::Task<Message>) {
            (Self::default(), Self::random_joke_task())
        }

        pub fn update(&mut self, message: Message) -> Action {
            match message {
                Message::Submit => Action::Submitted(self.content.clone()),
                Message::SetChanged(content) => {
                    self.content = content;
                    Action::None
                }
                Message::RandomJoke => Action::Task(Self::random_joke_task()),
            }
        }

        pub fn view(&self) -> iced::Element<Message> {
            iced::widget::column![
                iced::widget::text_input("Content", &self.content)
                    .on_input(Message::SetChanged),
                iced::widget::button("Random Joke").on_press(Message::RandomJoke),
                iced::widget::button("Submit").on_press(Message::Submit)
            ]
            .into()
        }

        fn random_joke_task() -> iced::Task<Message> {
            iced::Task::future(async {
                // Fetch a joke from the internet
                let client = reqwest::Client::new();
                let response: serde_json::Value = client
                    .get("https://icanhazdadjoke.com")
                    .header("Accept", "application/json")
                    .send()
                    .await
                    .unwrap()
                    .json()
                    .await
                    .unwrap();

                // Parse the response
                let joke = response["joke"].as_str().unwrap();

                // Return the joke as a message
                Message::Initialize(joke.to_owned())
            })
        }
    }
}